tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: https://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See CLI examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5<a href="https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"><img src="https://github.com/Tim55667757/TKSBrokerAPI/blob/develop/docs/media/TKSBrokerAPI-Logo.png?raw=true" alt="TKSBrokerAPI-Logo" width="780" target="_blank" /></a> 6 7**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 8as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 9from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 10 11TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 12the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 13 14- **Open account for trading:** https://tinkoff.ru/sl/AaX1Et1omnH 15- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 16- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 17- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 18- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 19- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 20""" 21 22# Copyright (c) 2022 Gilmillin Timur Mansurovich 23# 24# Licensed under the Apache License, Version 2.0 (the "License"); 25# you may not use this file except in compliance with the License. 26# You may obtain a copy of the License at 27# 28# http://www.apache.org/licenses/LICENSE-2.0 29# 30# Unless required by applicable law or agreed to in writing, software 31# distributed under the License is distributed on an "AS IS" BASIS, 32# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33# See the License for the specific language governing permissions and 34# limitations under the License. 35 36 37import sys 38import os 39from argparse import ArgumentParser 40from importlib.metadata import version 41 42from dateutil.tz import tzlocal 43from time import sleep 44 45import re 46import json 47import requests 48import traceback as tb 49from typing import Union 50 51from multiprocessing import cpu_count, Lock 52from multiprocessing.pool import ThreadPool 53import pandas as pd 54 55from mako.template import Template # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 56from Templates import * # Some html-templates used by reporting methods in TKSBrokerAPI module 57from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 58from TradeRoutines import * # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module 59 60from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator) 61from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 62 63import UniLogger as uLog # Logger for TKSBrokerAPI 64 65 66# --- Common technical parameters: 67 68PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 69uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 70uLogger.level = 10 # debug level by default for TKSBrokerAPI module 71uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 72 73__version__ = "1.6" # The "major.minor" version setup here, but build number define at the build-server only 74 75CPU_COUNT = cpu_count() # host's real CPU count 76CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 77 78 79class TinkoffBrokerServer: 80 """ 81 This class implements methods to work with Tinkoff broker server. 82 83 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 84 85 About `token`: https://tinkoff.github.io/investAPI/token/ 86 """ 87 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 88 """ 89 Main class init. 90 91 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 92 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 93 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 94 :param useCache: use default cache file with raw data to use instead of `iList`. 95 True by default. Cache is auto-update if new day has come. 96 If you don't want to use cache and always updates raw data then set `useCache=False`. 97 :param defaultCache: path to default cache file. `dump.json` by default. 98 """ 99 if token is None or not token: 100 try: 101 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 102 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 103 104 except KeyError: 105 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 106 raise Exception("Token required") 107 108 else: 109 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 110 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 111 112 if accountId is None or not accountId: 113 try: 114 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 115 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 116 117 except KeyError: 118 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 119 120 else: 121 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 122 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 123 124 self.version = __version__ # duplicate here used TKSBrokerAPI main version 125 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 126 127 Latest version: https://pypi.org/project/tksbrokerapi/ 128 """ 129 130 self._tag = "" 131 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 132 133 self.__lock = Lock() # initialize multiprocessing mutex lock 134 135 self._precision = 4 # precision, signs after comma, e.g. 2 for instruments like PLZL, 4 for instruments like USDRUB, if -1 then auto detect it when load data-file 136 137 self.aliases = TKS_TICKER_ALIASES 138 """Some aliases instead official tickers. 139 140 See also: `TKSEnums.TKS_TICKER_ALIASES` 141 """ 142 143 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 144 145 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 146 147 self._ticker = "" 148 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 149 150 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 151 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 152 153 See also: `SearchByTicker()`, `SearchInstruments()`. 154 """ 155 156 self._figi = "" 157 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 158 159 See also: `SearchByFIGI()`, `SearchInstruments()`. 160 """ 161 162 self.depth = 1 163 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 164 165 See also: `GetCurrentPrices()`. 166 """ 167 168 self.server = r"https://invest-public-api.tinkoff.ru/rest" 169 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 170 171 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 172 """ 173 174 uLogger.debug("Broker API server: {}".format(self.server)) 175 176 self.timeout = 15 177 """Server operations timeout in seconds. Default: `15`. 178 179 See also: `SendAPIRequest()`. 180 """ 181 182 self.headers = { 183 "Content-Type": "application/json", 184 "accept": "application/json", 185 "Authorization": "Bearer {}".format(self.token), 186 "x-app-name": "Tim55667757.TKSBrokerAPI", 187 } 188 """ 189 Headers which send in every request to broker server. Please, do not change it! 190 Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`. 191 192 See also: `SendAPIRequest()`. 193 """ 194 195 self.body = None 196 """Request body which send to broker server. Default: `None`. 197 198 See also: `SendAPIRequest()`. 199 """ 200 201 self.moreDebug = False 202 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 203 204 self.useHTMLReports = False 205 """ 206 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 207 208 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 209 """ 210 211 self.historyFile = None 212 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 213 214 See also: `History()`. 215 """ 216 217 self.htmlHistoryFile = "index.html" 218 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 219 220 See also: `ShowHistoryChart()`. 221 """ 222 223 self.instrumentsFile = "instruments.md" 224 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 225 226 See also: `ShowInstrumentsInfo()`. 227 """ 228 229 self.searchResultsFile = "search-results.md" 230 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 231 232 See also: `SearchInstruments()`. 233 """ 234 235 self.pricesFile = "prices.md" 236 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 237 238 See also: `GetListOfPrices()`. 239 """ 240 241 self.infoFile = "info.md" 242 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 243 244 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 245 """ 246 247 self.bondsXLSXFile = "ext-bonds.xlsx" 248 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 249 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 250 251 See also: `ExtendBondsData()`. 252 """ 253 254 self.calendarFile = "calendar.md" 255 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 256 257 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 258 259 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 260 """ 261 262 self.overviewFile = "overview.md" 263 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 264 265 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 266 """ 267 268 self.overviewDigestFile = "overview-digest.md" 269 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 270 271 See also: `Overview()` with parameter `details="digest"`. 272 """ 273 274 self.overviewPositionsFile = "overview-positions.md" 275 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 276 277 See also: `Overview()` with parameter `details="positions"`. 278 """ 279 280 self.overviewOrdersFile = "overview-orders.md" 281 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 282 283 See also: `Overview()` with parameter `details="orders"`. 284 """ 285 286 self.overviewAnalyticsFile = "overview-analytics.md" 287 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 288 289 See also: `Overview()` with parameter `details="analytics"`. 290 """ 291 292 self.overviewBondsCalendarFile = "overview-calendar.md" 293 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 294 295 See also: `Overview()` with parameter `details="calendar"`. 296 """ 297 298 self.reportFile = "deals.md" 299 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 300 301 See also: `Deals()`. 302 """ 303 304 self.withdrawalLimitsFile = "limits.md" 305 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 306 307 See also: `OverviewLimits()` and `RequestLimits()`. 308 """ 309 310 self.userInfoFile = "user-info.md" 311 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 312 313 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 314 """ 315 316 self.userAccountsFile = "accounts.md" 317 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 318 319 See also: `OverviewAccounts()`, `RequestAccounts()`. 320 """ 321 322 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 323 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 324 325 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 326 327 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 328 """ 329 330 self.iList = None # init iList for raw instruments data 331 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 332 333 See also: `Listing()`, `DumpInstruments()`. 334 """ 335 336 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 337 if useCache: 338 if os.path.exists(self.iListDumpFile): 339 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 340 curTime = datetime.now(tzutc()) 341 342 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 343 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 344 345 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 346 347 else: 348 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 349 350 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 351 os.path.abspath(self.iListDumpFile), 352 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 353 )) 354 355 else: 356 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 357 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 358 359 else: 360 self.iList = self.Listing() # request new raw instruments data from broker server 361 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 362 363 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 364 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 365 366 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 367 """ 368 369 @property 370 def tag(self) -> str: 371 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 372 return self._tag 373 374 @tag.setter 375 def tag(self, value): 376 """Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 377 self._tag = str(value) 378 379 if self._tag: 380 for handler in uLogger.handlers: 381 handler.setFormatter(uLog.logging.Formatter(uLog.formatStringWithTag.format(tag=self._tag))) 382 383 uLogger.debug("Custom TKSBrokerAPI tag was set: {}".format(self._tag)) 384 385 else: 386 for handler in uLogger.handlers: 387 handler.setFormatter(uLog.logging.Formatter(uLog.formatString)) 388 389 uLogger.debug("Default logger format is used") 390 391 @property 392 def ticker(self) -> str: 393 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 394 395 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 396 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 397 398 See also: `SearchByTicker()`, `SearchInstruments()`. 399 """ 400 return self._ticker 401 402 @ticker.setter 403 def ticker(self, value): 404 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 405 406 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 407 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 408 409 See also: `SearchByTicker()`, `SearchInstruments()`. 410 """ 411 self._ticker = str(value).upper() # Tickers may be upper case only 412 413 @property 414 def figi(self) -> str: 415 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 416 417 See also: `SearchByFIGI()`, `SearchInstruments()`. 418 """ 419 return self._figi 420 421 @figi.setter 422 def figi(self, value): 423 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 424 425 See also: `SearchByFIGI()`, `SearchInstruments()`. 426 """ 427 self._figi = str(value).upper() # FIGI may be upper case only 428 429 def _ParseJSON(self, rawData="{}") -> dict: 430 """ 431 Parse JSON from response string. 432 433 :param rawData: this is a string with JSON-formatted text. 434 :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`. 435 """ 436 try: 437 responseJSON = json.loads(rawData) if rawData else {} 438 439 if self.moreDebug: 440 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 441 442 return responseJSON 443 444 except Exception as e: 445 uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e)) 446 447 return {} 448 449 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 450 """ 451 Send GET or POST request to broker server and receive JSON object. 452 453 self.header: must be defining with dictionary of headers. 454 self.body: if define then used as request body. None by default. 455 self.timeout: global request timeout, 15 seconds by default. 456 :param url: url with REST request. 457 :param reqType: send "GET" or "POST" request. "GET" by default. 458 :param retry: how many times retry after first request if an 5xx server errors occurred. 459 :param pause: sleep time in seconds between retries. 460 :return: response JSON (dictionary) from broker. 461 """ 462 if reqType.upper() not in ("GET", "POST"): 463 uLogger.error("You can define request type: `GET` or `POST`!") 464 raise Exception("Incorrect value") 465 466 if self.moreDebug: 467 uLogger.debug("Request parameters:") 468 uLogger.debug(" - REST API URL: {}".format(url)) 469 uLogger.debug(" - request type: {}".format(reqType)) 470 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 471 uLogger.debug(" - body:\n{}".format(self.body)) 472 473 # fast hack to avoid all operations with some tickers/FIGI 474 responseJSON = {} 475 oK = True 476 for item in self.exclude: 477 if item in url: 478 if self.moreDebug: 479 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 480 481 oK = False 482 break 483 484 if oK: 485 with self.__lock: # acquire the mutex lock 486 counter = 0 487 response = None 488 errMsg = "" 489 490 while not response and counter <= retry: 491 if reqType == "GET": 492 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 493 494 if reqType == "POST": 495 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 496 497 if self.moreDebug: 498 uLogger.debug("Response:") 499 uLogger.debug(" - status code: {}".format(response.status_code)) 500 uLogger.debug(" - reason: {}".format(response.reason)) 501 uLogger.debug(" - body length: {}".format(len(response.text))) 502 uLogger.debug(" - headers:\n{}".format(response.headers)) 503 504 # Server returns some headers: 505 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 506 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 507 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 508 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 509 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 510 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 511 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 512 sleep(rateLimitWait) 513 514 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 515 if 400 <= response.status_code < 500: 516 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 517 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 518 519 if "code" in response.text and "message" in response.text: 520 msgDict = self._ParseJSON(rawData=response.text) 521 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 522 523 counter = retry + 1 # do not retry for 4xx errors 524 525 if 500 <= response.status_code < 600: 526 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 527 uLogger.debug(" - not oK, {}".format(errMsg)) 528 529 if "code" in response.text and "message" in response.text: 530 errMsgDict = self._ParseJSON(rawData=response.text) 531 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 532 533 counter += 1 534 535 if counter <= retry: 536 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 537 sleep(pause) 538 539 responseJSON = self._ParseJSON(rawData=response.text) 540 541 if errMsg: 542 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 543 uLogger.error(" - not oK, {}".format(errMsg)) 544 545 return responseJSON 546 547 def _IUpdater(self, iType: str) -> tuple: 548 """ 549 Request instrument by type from server. See available API methods for instruments: 550 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 551 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 552 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 553 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 554 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 555 556 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 557 :return: tuple with iType name and list of available instruments of current type for defined user token. 558 """ 559 result = [] 560 561 if iType in TKS_INSTRUMENTS: 562 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 563 564 # all instruments have the same body in API v2 requests: 565 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 566 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 567 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 568 569 return iType, result 570 571 def _IWrapper(self, kwargs): 572 """ 573 Wrapper runs instrument's update method `_IUpdater()`. 574 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 575 """ 576 return self._IUpdater(**kwargs) 577 578 def Listing(self) -> dict: 579 """ 580 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 581 582 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 583 """ 584 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 585 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 586 587 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 588 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 589 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 590 591 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 592 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 593 poolUpdater.close() # close the thread pool 594 poolUpdater.join() # wait a moment until all data returns from threads 595 596 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 597 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 598 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 599 600 # calculate minimum price increment (step) for all instruments and set up instrument's type: 601 for iType in iList.keys(): 602 for ticker in iList[iType]: 603 iList[iType][ticker]["type"] = iType 604 605 if "minPriceIncrement" in iList[iType][ticker].keys(): 606 iList[iType][ticker]["step"] = NanoToFloat( 607 iList[iType][ticker]["minPriceIncrement"]["units"], 608 iList[iType][ticker]["minPriceIncrement"]["nano"], 609 ) 610 611 else: 612 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 613 614 return iList 615 616 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 617 """ 618 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 619 620 See also: `DumpInstruments()`, `Listing()`. 621 622 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 623 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 624 """ 625 if self.iListDumpFile is None or not self.iListDumpFile: 626 uLogger.error("Output name of dump file must be defined!") 627 raise Exception("Filename required") 628 629 if not self.iList or forceUpdate: 630 self.iList = self.Listing() 631 632 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 633 634 # Save as XLSX with separated sheets for every type of instruments: 635 with pd.ExcelWriter( 636 path=xlsxDumpFile, 637 date_format=TKS_DATE_FORMAT, 638 datetime_format=TKS_DATE_TIME_FORMAT, 639 mode="w", 640 ) as writer: 641 for iType in TKS_INSTRUMENTS: 642 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 643 df = df[sorted(df)] # sorted by column names 644 df = df.applymap( 645 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 646 na_action="ignore", 647 ) # converting numbers from nano-type to float in every cell 648 df.to_excel( 649 writer, 650 sheet_name=iType, 651 encoding="UTF-8", 652 freeze_panes=(1, 1), 653 ) # saving as XLSX-file with freeze first row and column as headers 654 655 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 656 657 def DumpInstruments(self, forceUpdate: bool = True) -> str: 658 """ 659 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 660 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 661 662 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 663 664 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 665 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 666 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 667 """ 668 if self.iListDumpFile is None or not self.iListDumpFile: 669 uLogger.error("Output name of dump file must be defined!") 670 raise Exception("Filename required") 671 672 if not self.iList or forceUpdate: 673 self.iList = self.Listing() 674 675 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 676 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 677 fH.write(jsonDump) 678 679 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 680 681 return jsonDump 682 683 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str: 684 """ 685 Show information about one instrument defined by json data and prints it in Markdown format. 686 687 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 688 689 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 690 :param show: if `True` then also printing information about instrument and its current price. 691 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 692 :return: multilines text in Markdown format with information about one instrument. 693 """ 694 splitLine = "| | |\n" 695 infoText = "" 696 697 if iJSON is not None and iJSON and isinstance(iJSON, dict): 698 info = [ 699 "# Main information\n\n", 700 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 701 "| Parameters | Values |\n", 702 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 703 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 704 "| Full name: | {:<54} |\n".format(iJSON["name"]), 705 ] 706 707 if "sector" in iJSON.keys() and iJSON["sector"]: 708 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 709 710 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 711 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 712 713 info.extend([ 714 splitLine, 715 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 716 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 717 ]) 718 719 if "isin" in iJSON.keys() and iJSON["isin"]: 720 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 721 722 if "classCode" in iJSON.keys(): 723 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 724 725 info.extend([ 726 splitLine, 727 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 728 splitLine, 729 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 730 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 731 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 732 ]) 733 734 if iJSON["figi"]: 735 self._figi = iJSON["figi"] 736 iJSON = iJSON | self.RequestTradingStatus() 737 738 info.extend([ 739 splitLine, 740 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 741 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 742 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 743 ]) 744 745 info.append(splitLine) 746 747 if "type" in iJSON.keys() and iJSON["type"]: 748 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 749 750 if "shareType" in iJSON.keys() and iJSON["shareType"]: 751 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 752 753 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 754 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 755 756 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 757 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 758 759 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 760 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 761 762 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 763 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 764 765 if "focusType" in iJSON.keys() and iJSON["focusType"]: 766 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 767 768 if "assetType" in iJSON.keys() and iJSON["assetType"]: 769 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 770 771 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 772 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 773 774 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 775 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 776 777 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 778 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 779 780 if "currency" in iJSON.keys(): 781 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 782 783 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 784 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 785 786 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 787 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 788 789 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 790 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 791 792 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 793 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 794 795 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 796 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 797 798 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 799 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 800 801 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 802 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 803 804 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 805 info.append("| Perpetual bond: | Yes |\n") 806 807 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 808 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 809 810 iExt = None 811 if iJSON["type"] == "Bonds": 812 info.extend([ 813 splitLine, 814 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 815 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 816 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 817 iJSON["nominal"]["currency"], 818 )), 819 ]) 820 821 if "floatingCouponFlag" in iJSON.keys(): 822 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 823 824 if "amortizationFlag" in iJSON.keys(): 825 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 826 827 info.append(splitLine) 828 829 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 830 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 831 832 if iJSON["figi"]: 833 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 834 835 info.extend([ 836 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 837 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 838 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 839 ]) 840 841 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 842 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 843 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 844 iJSON["aciValue"]["currency"] 845 ))) 846 847 if "currentPrice" in iJSON.keys(): 848 info.append(splitLine) 849 850 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 851 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 852 853 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 854 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 855 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 856 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 857 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 858 859 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 860 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 861 862 info.extend([ 863 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 864 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 865 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 866 )), 867 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 868 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 869 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 870 )), 871 "| Changes between last deal price and last close | {:<54} |\n".format( 872 "{:.2f}%{}".format( 873 iJSON["currentPrice"]["changes"], 874 " ({}{:.2f} {})".format( 875 "+" if bondChangesDelta > 0 else "", 876 bondChangesDelta, 877 aciCurrency 878 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 879 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 880 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 881 currency 882 ), 883 ) 884 ), 885 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 886 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 887 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 888 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 889 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 890 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 891 )), 892 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 893 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 894 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 895 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 896 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 897 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 898 )), 899 ]) 900 901 if "lot" in iJSON.keys(): 902 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 903 904 if "step" in iJSON.keys() and iJSON["step"] != 0: 905 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 906 907 # Add bond payment calendar: 908 if iJSON["type"] == "Bonds": 909 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 910 info.extend(["\n#", strCalendar]) 911 912 infoText += "".join(info) 913 914 if show and not onlyFiles: 915 uLogger.info("{}".format(infoText)) 916 917 if self.infoFile is not None and (show or onlyFiles): 918 with open(self.infoFile, "w", encoding="UTF-8") as fH: 919 fH.write(infoText) 920 921 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 922 923 if self.useHTMLReports: 924 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 925 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 926 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 927 928 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 929 930 return infoText 931 932 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 933 """ 934 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 935 936 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 937 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 938 :return: JSON formatted data with information about instrument. 939 """ 940 tickerJSON = {} 941 if self.moreDebug: 942 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 943 944 if not self._ticker: 945 uLogger.warning("self._ticker variable is not be empty!") 946 947 else: 948 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 949 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 950 raise Exception("Instrument not allowed") 951 952 if not self.iList: 953 self.iList = self.Listing() 954 955 if self._ticker in self.iList["Shares"].keys(): 956 tickerJSON = self.iList["Shares"][self._ticker] 957 if self.moreDebug: 958 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 959 960 elif self._ticker in self.iList["Currencies"].keys(): 961 tickerJSON = self.iList["Currencies"][self._ticker] 962 if self.moreDebug: 963 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 964 965 elif self._ticker in self.iList["Bonds"].keys(): 966 tickerJSON = self.iList["Bonds"][self._ticker] 967 if self.moreDebug: 968 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 969 970 elif self._ticker in self.iList["Etfs"].keys(): 971 tickerJSON = self.iList["Etfs"][self._ticker] 972 if self.moreDebug: 973 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 974 975 elif self._ticker in self.iList["Futures"].keys(): 976 tickerJSON = self.iList["Futures"][self._ticker] 977 if self.moreDebug: 978 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 979 980 if tickerJSON: 981 self._figi = tickerJSON["figi"] 982 983 if requestPrice: 984 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 985 986 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 987 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 988 989 else: 990 tickerJSON["currentPrice"]["changes"] = 0 991 992 if show: 993 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 994 995 else: 996 if show: 997 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 998 999 return tickerJSON 1000 1001 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1002 """ 1003 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1004 1005 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1006 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1007 :return: JSON formatted data with information about instrument. 1008 """ 1009 figiJSON = {} 1010 if self.moreDebug: 1011 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 1012 1013 if not self._figi: 1014 uLogger.warning("self._figi variable is not be empty!") 1015 1016 else: 1017 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1018 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 1019 raise Exception("Instrument not allowed") 1020 1021 if not self.iList: 1022 self.iList = self.Listing() 1023 1024 for item in self.iList["Shares"].keys(): 1025 if self._figi == self.iList["Shares"][item]["figi"]: 1026 figiJSON = self.iList["Shares"][item] 1027 1028 if self.moreDebug: 1029 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1030 1031 break 1032 1033 if not figiJSON: 1034 for item in self.iList["Currencies"].keys(): 1035 if self._figi == self.iList["Currencies"][item]["figi"]: 1036 figiJSON = self.iList["Currencies"][item] 1037 1038 if self.moreDebug: 1039 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1040 1041 break 1042 1043 if not figiJSON: 1044 for item in self.iList["Bonds"].keys(): 1045 if self._figi == self.iList["Bonds"][item]["figi"]: 1046 figiJSON = self.iList["Bonds"][item] 1047 1048 if self.moreDebug: 1049 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1050 1051 break 1052 1053 if not figiJSON: 1054 for item in self.iList["Etfs"].keys(): 1055 if self._figi == self.iList["Etfs"][item]["figi"]: 1056 figiJSON = self.iList["Etfs"][item] 1057 1058 if self.moreDebug: 1059 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1060 1061 break 1062 1063 if not figiJSON: 1064 for item in self.iList["Futures"].keys(): 1065 if self._figi == self.iList["Futures"][item]["figi"]: 1066 figiJSON = self.iList["Futures"][item] 1067 1068 if self.moreDebug: 1069 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1070 1071 break 1072 1073 if figiJSON: 1074 self._figi = figiJSON["figi"] 1075 self._ticker = figiJSON["ticker"] 1076 1077 if requestPrice: 1078 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1079 1080 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1081 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1082 1083 else: 1084 figiJSON["currentPrice"]["changes"] = 0 1085 1086 if show: 1087 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1088 1089 else: 1090 if show: 1091 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1092 1093 return figiJSON 1094 1095 def GetCurrentPrices(self, show: bool = True) -> dict: 1096 """ 1097 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1098 `{"buy": [{"price": 1243.8, "quantity": 193}, 1099 {"price": 1244.0, "quantity": 168}, 1100 {"price": 1244.8, "quantity": 5}, 1101 {"price": 1245.0, "quantity": 61}, 1102 {"price": 1245.4, "quantity": 60}], 1103 "sell": [{"price": 1243.6, "quantity": 8}, 1104 {"price": 1242.6, "quantity": 10}, 1105 {"price": 1242.4, "quantity": 18}, 1106 {"price": 1242.2, "quantity": 50}, 1107 {"price": 1242.0, "quantity": 113}], 1108 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1109 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1110 - sell: list of dicts with Buyers prices, 1111 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1112 - quantity: volume value by current price in lots, 1113 - limitUp: current trade session limit price, maximum, 1114 - limitDown: current trade session limit price, minimum, 1115 - lastPrice: last deal price of the instrument, 1116 - closePrice: previous trade session close price of the instrument. 1117 1118 See also: `SearchByTicker()` and `SearchByFIGI()`. 1119 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1120 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1121 1122 :param show: if `True` then print DOM to log and console. 1123 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1124 If an error occurred then returns an empty record: 1125 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1126 """ 1127 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1128 1129 if self.depth < 1: 1130 uLogger.error("Depth of Market (DOM) must be >=1!") 1131 raise Exception("Incorrect value") 1132 1133 if not (self._ticker or self._figi): 1134 uLogger.error("self._ticker or self._figi variables must be defined!") 1135 raise Exception("Ticker or FIGI required") 1136 1137 if self._ticker and not self._figi: 1138 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1139 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1140 1141 if not self._ticker and self._figi: 1142 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1143 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1144 1145 if not self._figi: 1146 uLogger.error("FIGI is not defined!") 1147 raise Exception("Ticker or FIGI required") 1148 1149 else: 1150 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1151 1152 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1153 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1154 self.body = str({"figi": self._figi, "depth": self.depth}) 1155 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1156 1157 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1158 # list of dicts with sellers orders: 1159 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1160 1161 # list of dicts with buyers orders: 1162 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1163 1164 # max price of instrument at this time: 1165 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1166 1167 # min price of instrument at this time: 1168 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1169 1170 # last price of deal with instrument: 1171 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1172 1173 # last close price of instrument: 1174 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1175 1176 else: 1177 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1178 uLogger.debug("Server response: {}".format(pricesResponse)) 1179 1180 if show: 1181 if prices["buy"] or prices["sell"]: 1182 info = [ 1183 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1184 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1185 self._ticker, 1186 self._figi, 1187 self.depth, 1188 ), 1189 "-" * 60, "\n", 1190 " Orders of Buyers | Orders of Sellers\n", 1191 "-" * 60, "\n", 1192 " Sell prices (volumes) | Buy prices (volumes)\n", 1193 "-" * 60, "\n", 1194 ] 1195 1196 if not prices["buy"]: 1197 info.append(" | No orders!\n") 1198 sumBuy = 0 1199 1200 else: 1201 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1202 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1203 for item in maxMinSorted: 1204 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1205 1206 if not prices["sell"]: 1207 info.append("No orders! |\n") 1208 sumSell = 0 1209 1210 else: 1211 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1212 for item in prices["sell"]: 1213 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1214 1215 info.extend([ 1216 "-" * 60, "\n", 1217 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1218 "-" * 60, "\n", 1219 ]) 1220 1221 infoText = "".join(info) 1222 1223 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1224 1225 else: 1226 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1227 1228 return prices 1229 1230 def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str: 1231 """ 1232 This method get and show information about all available broker instruments for current user account. 1233 If `instrumentsFile` string is not empty then also save information to this file. 1234 1235 :param show: if `True` then print results to console, if `False` — print only to file. 1236 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1237 :return: multi-lines string with all available broker instruments. 1238 """ 1239 if not self.iList: 1240 self.iList = self.Listing() 1241 1242 info = [ 1243 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1244 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1245 ] 1246 1247 # add instruments count by type: 1248 for iType in self.iList.keys(): 1249 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1250 1251 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1252 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1253 1254 # generating info tables with all instruments by type: 1255 for iType in self.iList.keys(): 1256 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1257 1258 for instrument in self.iList[iType].keys(): 1259 iName = self.iList[iType][instrument]["name"] # instrument's name 1260 if len(iName) > 57: 1261 iName = "{}...".format(iName[:54]) # right trim for a long string 1262 1263 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1264 self.iList[iType][instrument]["ticker"], 1265 iName, 1266 self.iList[iType][instrument]["figi"], 1267 self.iList[iType][instrument]["currency"], 1268 self.iList[iType][instrument]["lot"], 1269 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1270 )) 1271 1272 infoText = "".join(info) 1273 1274 if show and not onlyFiles: 1275 uLogger.info(infoText) 1276 1277 if self.instrumentsFile and (show or onlyFiles): 1278 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1279 fH.write(infoText) 1280 1281 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1282 1283 if self.useHTMLReports: 1284 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1285 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1286 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1287 1288 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1289 1290 return infoText 1291 1292 def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict: 1293 """ 1294 This method search and show information about instruments by part of its ticker, FIGI or name. 1295 If `searchResultsFile` string is not empty then also save information to this file. 1296 1297 :param pattern: string with part of ticker, FIGI or instrument's name. 1298 :param show: if `True` then print results to console, if `False` — return list of result only. 1299 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1300 :return: list of dictionaries with all found instruments. 1301 """ 1302 if not self.iList: 1303 self.iList = self.Listing() 1304 1305 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1306 compiledPattern = re.compile(pattern, re.IGNORECASE) 1307 1308 for iType in self.iList: 1309 for instrument in self.iList[iType].values(): 1310 searchResult = compiledPattern.search(" ".join( 1311 [instrument["ticker"], instrument["figi"], instrument["name"]] 1312 )) 1313 1314 if searchResult: 1315 searchResults[iType][instrument["ticker"]] = instrument 1316 1317 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1318 info = [ 1319 "# Search results\n\n", 1320 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1321 "* **Search pattern:** [{}]\n".format(pattern), 1322 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1323 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1324 ] 1325 infoShort = info[:] 1326 1327 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1328 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1329 skippedLine = "| ... | ... | ... | ... |\n" 1330 1331 if resultsLen == 0: 1332 info.append("\nNo results\n") 1333 infoShort.append("\nNo results\n") 1334 uLogger.warning("No results. Try changing your search pattern.") 1335 1336 else: 1337 for iType in searchResults: 1338 iTypeValuesCount = len(searchResults[iType].values()) 1339 if iTypeValuesCount > 0: 1340 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1341 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1342 1343 for instrument in searchResults[iType].values(): 1344 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1345 instrument["type"], 1346 instrument["ticker"], 1347 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1348 instrument["figi"], 1349 )) 1350 1351 if iTypeValuesCount <= 5: 1352 infoShort.extend(info[-iTypeValuesCount:]) 1353 1354 else: 1355 infoShort.extend(info[-5:]) 1356 infoShort.append(skippedLine) 1357 1358 infoText = "".join(info) 1359 infoTextShort = "".join(infoShort) 1360 1361 if show and not onlyFiles: 1362 uLogger.info(infoTextShort) 1363 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1364 1365 if self.searchResultsFile and (show or onlyFiles): 1366 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1367 fH.write(infoText) 1368 1369 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1370 1371 if self.useHTMLReports: 1372 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1373 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1374 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1375 1376 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1377 1378 return searchResults 1379 1380 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1381 """ 1382 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1383 1384 :param instruments: list of strings with tickers or FIGIs. 1385 :return: list with unique instrument FIGIs only. 1386 """ 1387 requestedInstruments = [] 1388 for iName in instruments: 1389 if iName not in self.aliases.keys(): 1390 if iName not in requestedInstruments: 1391 requestedInstruments.append(iName) 1392 1393 else: 1394 if iName not in requestedInstruments: 1395 if self.aliases[iName] not in requestedInstruments: 1396 requestedInstruments.append(self.aliases[iName]) 1397 1398 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1399 1400 onlyUniqueFIGIs = [] 1401 for iName in requestedInstruments: 1402 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1403 continue 1404 1405 self._ticker = iName 1406 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1407 1408 if not iData: 1409 self._ticker = "" 1410 self._figi = iName 1411 1412 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1413 1414 if not iData: 1415 self._figi = "" 1416 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1417 1418 if iData and iData["figi"] not in onlyUniqueFIGIs: 1419 onlyUniqueFIGIs.append(iData["figi"]) 1420 1421 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1422 1423 return onlyUniqueFIGIs 1424 1425 def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]: 1426 """ 1427 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1428 1429 See limits: https://tinkoff.github.io/investAPI/limits/ 1430 1431 If `pricesFile` string is not empty then also save information to this file. 1432 1433 :param instruments: list of strings with tickers or FIGIs. 1434 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1435 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1436 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1437 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1438 """ 1439 if instruments is None or not instruments: 1440 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1441 raise Exception("Ticker or FIGI required") 1442 1443 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1444 1445 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1446 1447 iList = [] # trying to get info and current prices about all unique instruments: 1448 for self._figi in onlyUniqueFIGIs: 1449 iData = self.SearchByFIGI(requestPrice=True, show=False) 1450 iList.append(iData) 1451 1452 self.ShowListOfPrices(iList, show, onlyFiles) 1453 1454 return iList 1455 1456 def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str: 1457 """ 1458 Show table contains current prices of given instruments. 1459 1460 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1461 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1462 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1463 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1464 :return: multilines text in Markdown format as a table contains current prices. 1465 """ 1466 infoText = "" 1467 1468 if show or self.pricesFile or onlyFiles: 1469 info = [ 1470 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1471 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1472 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1473 ] 1474 1475 for item in iList: 1476 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1477 item["ticker"], 1478 item["figi"], 1479 item["type"], 1480 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1481 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1482 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1483 "{} / {}".format( 1484 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1485 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1486 ), 1487 "{} / {}".format( 1488 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1489 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1490 ), 1491 item["currency"], 1492 )) 1493 1494 infoText = "".join(info) 1495 1496 if show and not onlyFiles: 1497 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1498 1499 if self.pricesFile and (show or onlyFiles): 1500 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1501 fH.write(infoText) 1502 1503 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1504 1505 if self.useHTMLReports: 1506 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1507 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1508 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1509 1510 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1511 1512 return infoText 1513 1514 def RequestTradingStatus(self) -> dict: 1515 """ 1516 Requesting trading status for the instrument defined by `figi` variable. 1517 1518 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1519 1520 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1521 1522 :return: dictionary with trading status attributes. Response example: 1523 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1524 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1525 """ 1526 if self._figi is None or not self._figi: 1527 uLogger.error("Variable `figi` must be defined for using this method!") 1528 raise Exception("FIGI required") 1529 1530 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1531 1532 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1533 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1534 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1535 1536 if self.moreDebug: 1537 uLogger.debug("Records about current trading status successfully received") 1538 1539 return tradingStatus 1540 1541 def RequestPortfolio(self) -> dict: 1542 """ 1543 Requesting actual user's portfolio for current `accountId`. 1544 1545 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1546 1547 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1548 1549 :return: dictionary with user's portfolio. 1550 """ 1551 if self.accountId is None or not self.accountId: 1552 uLogger.error("Variable `accountId` must be defined for using this method!") 1553 raise Exception("Account ID required") 1554 1555 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1556 1557 self.body = str({"accountId": self.accountId}) 1558 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1559 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1560 1561 if self.moreDebug: 1562 uLogger.debug("Records about user's portfolio successfully received") 1563 1564 return rawPortfolio 1565 1566 def RequestPositions(self) -> dict: 1567 """ 1568 Requesting open positions by currencies and instruments for current `accountId`. 1569 1570 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1571 1572 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1573 1574 :return: dictionary with open positions by instruments. 1575 """ 1576 if self.accountId is None or not self.accountId: 1577 uLogger.error("Variable `accountId` must be defined for using this method!") 1578 raise Exception("Account ID required") 1579 1580 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1581 1582 self.body = str({"accountId": self.accountId}) 1583 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1584 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1585 1586 if self.moreDebug: 1587 uLogger.debug("Records about current open positions successfully received") 1588 1589 return rawPositions 1590 1591 def RequestPendingOrders(self) -> list: 1592 """ 1593 Requesting current actual pending limit orders for current `accountId`. 1594 1595 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1596 1597 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1598 1599 :return: list of dictionaries with pending limit orders. 1600 """ 1601 if self.accountId is None or not self.accountId: 1602 uLogger.error("Variable `accountId` must be defined for using this method!") 1603 raise Exception("Account ID required") 1604 1605 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1606 1607 self.body = str({"accountId": self.accountId}) 1608 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1609 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1610 1611 if "orders" in rawResponse.keys(): 1612 rawOrders = rawResponse["orders"] 1613 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1614 1615 else: 1616 rawOrders = [] 1617 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1618 1619 return rawOrders 1620 1621 def RequestStopOrders(self) -> list: 1622 """ 1623 Requesting current actual stop orders for current `accountId`. 1624 1625 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1626 1627 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1628 1629 :return: list of dictionaries with stop orders. 1630 """ 1631 if self.accountId is None or not self.accountId: 1632 uLogger.error("Variable `accountId` must be defined for using this method!") 1633 raise Exception("Account ID required") 1634 1635 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1636 1637 self.body = str({"accountId": self.accountId}) 1638 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1639 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1640 1641 if "stopOrders" in rawResponse.keys(): 1642 rawStopOrders = rawResponse["stopOrders"] 1643 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1644 1645 else: 1646 rawStopOrders = [] 1647 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1648 1649 return rawStopOrders 1650 1651 def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict: 1652 """ 1653 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1654 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1655 and `overviewBondsCalendarFile` are defined then also save information to file. 1656 1657 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1658 many requests about the state of the portfolio, and then, based on the received data, a large number 1659 of calculation and statistics are collected. 1660 1661 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1662 :param details: how detailed should the information be? 1663 - `full` — shows full available information about portfolio status (by default), 1664 - `positions` — shows only open positions, 1665 - `orders` — shows only sections of open limits and stop orders. 1666 - `digest` — show a short digest of the portfolio status, 1667 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1668 - `calendar` — shows only the bonds calendar section (if these present in portfolio). 1669 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1670 :return: dictionary with client's raw portfolio and some statistics. 1671 """ 1672 if self.accountId is None or not self.accountId: 1673 uLogger.error("Variable `accountId` must be defined for using this method!") 1674 raise Exception("Account ID required") 1675 1676 view = { 1677 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1678 "headers": {}, # list of dictionaries, response headers without "positions" section 1679 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1680 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1681 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1682 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1683 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1684 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1685 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1686 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1687 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1688 }, 1689 "stat": { # --- some statistics calculated using "raw" sections: 1690 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1691 "availableRUB": 0., # available rubles (without other currencies) 1692 "blockedRUB": 0., # blocked sum in Russian Rouble 1693 "totalChangesRUB": 0., # changes for all open trades in RUB 1694 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1695 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1696 "sharesCostRUB": 0., # costs of all shares in RUB 1697 "bondsCostRUB": 0., # costs of all bonds in RUB 1698 "etfsCostRUB": 0., # costs of all etfs in RUB 1699 "futuresCostRUB": 0., # costs of all futures in RUB 1700 "Currencies": [], # list of dictionaries of all currencies statistics 1701 "Shares": [], # list of dictionaries of all shares statistics 1702 "Bonds": [], # list of dictionaries of all bonds statistics 1703 "Etfs": [], # list of dictionaries of all etfs statistics 1704 "Futures": [], # list of dictionaries of all futures statistics 1705 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1706 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1707 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1708 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1709 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1710 }, 1711 "analytics": { # --- some analytics of portfolio: 1712 "distrByAssets": {}, # portfolio distribution by assets 1713 "distrByCompanies": {}, # portfolio distribution by companies 1714 "distrBySectors": {}, # portfolio distribution by sectors 1715 "distrByCurrencies": {}, # portfolio distribution by currencies 1716 "distrByCountries": {}, # portfolio distribution by countries 1717 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1718 } 1719 } 1720 1721 details = details.lower() 1722 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1723 if details not in availableDetails: 1724 details = "full" 1725 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1726 1727 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1728 1729 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1730 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1731 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1732 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1733 1734 # save response headers without "positions" section: 1735 for key in portfolioResponse.keys(): 1736 if key != "positions": 1737 view["raw"]["headers"][key] = portfolioResponse[key] 1738 1739 else: 1740 continue 1741 1742 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1743 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1744 for item in portfolioResponse["positions"]: 1745 if item["instrumentType"] == "currency": 1746 self._figi = item["figi"] 1747 if not self._figi and item["ticker"]: 1748 self._ticker = item["ticker"] 1749 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1750 1751 curr = self.SearchByFIGI(requestPrice=False) 1752 1753 # current price of currency in RUB: 1754 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1755 "name": curr["name"], 1756 "currentPrice": NanoToFloat( 1757 item["currentPrice"]["units"], 1758 item["currentPrice"]["nano"] 1759 ), 1760 } 1761 1762 view["raw"]["Currencies"].append(item) 1763 1764 elif item["instrumentType"] == "share": 1765 view["raw"]["Shares"].append(item) 1766 1767 elif item["instrumentType"] == "bond": 1768 view["raw"]["Bonds"].append(item) 1769 1770 elif item["instrumentType"] == "etf": 1771 view["raw"]["Etfs"].append(item) 1772 1773 elif item["instrumentType"] == "futures": 1774 view["raw"]["Futures"].append(item) 1775 1776 else: 1777 continue 1778 1779 # how many volume of currencies (by ISO currency name) are blocked: 1780 for item in view["raw"]["positions"]["blocked"]: 1781 blocked = NanoToFloat(item["units"], item["nano"]) 1782 if blocked > 0: 1783 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1784 1785 # how many volume of instruments (by FIGI) are blocked: 1786 for item in view["raw"]["positions"]["securities"]: 1787 blocked = int(item["blocked"]) 1788 if blocked > 0: 1789 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1790 1791 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1792 1793 if "rub" in allBlocked.keys(): 1794 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1795 1796 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1797 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1798 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1799 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1800 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1801 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1802 view["stat"]["portfolioCostRUB"] = sum([ 1803 view["stat"]["allCurrenciesCostRUB"], 1804 view["stat"]["sharesCostRUB"], 1805 view["stat"]["bondsCostRUB"], 1806 view["stat"]["etfsCostRUB"], 1807 view["stat"]["futuresCostRUB"], 1808 ]) 1809 1810 # --- calculating some portfolio statistics: 1811 byComp = {} # distribution by companies 1812 bySect = {} # distribution by sectors 1813 byCurr = {} # distribution by currencies (include RUB) 1814 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1815 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1816 1817 for item in portfolioResponse["positions"]: 1818 self._figi = item["figi"] 1819 if not self._figi and item["ticker"]: 1820 self._ticker = item["ticker"] 1821 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1822 1823 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1824 1825 if instrument: 1826 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1827 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1828 1829 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1830 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1831 1832 else: 1833 blocked = 0 1834 1835 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1836 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1837 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1838 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1839 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1840 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1841 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1842 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1843 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1844 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1845 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1846 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1847 1848 statData = { 1849 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1850 "ticker": instrument["ticker"], # ticker by FIGI 1851 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1852 "volume": volume, # available volume of instrument 1853 "lots": lots, # volume in lots of instrument 1854 "direction": direction, # direction of an instrument's position: short or long 1855 "blocked": blocked, # blocked volume of currency or instrument 1856 "currentPrice": curPrice, # current instrument's price in basic asset 1857 "average": average, # current average position price 1858 "cost": cost, # current cost of all volume of instrument in basic asset 1859 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1860 "costRUB": costRUB, # cost of instrument in ruble 1861 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1862 "profit": profit, # expected profit at current moment 1863 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1864 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1865 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1866 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1867 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1868 "step": instrument["step"], # minimum price increment 1869 } 1870 1871 # adding distribution by unique countries: 1872 if statData["country"] not in byCountry.keys(): 1873 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1874 1875 else: 1876 byCountry[statData["country"]]["cost"] += costRUB 1877 byCountry[statData["country"]]["percent"] += percentCostRUB 1878 1879 if item["instrumentType"] != "currency": 1880 # adding distribution by unique companies: 1881 if statData["name"]: 1882 if statData["name"] not in byComp.keys(): 1883 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1884 1885 else: 1886 byComp[statData["name"]]["cost"] += costRUB 1887 byComp[statData["name"]]["percent"] += percentCostRUB 1888 1889 # adding distribution by unique sectors: 1890 if statData["sector"] not in bySect.keys(): 1891 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1892 1893 else: 1894 bySect[statData["sector"]]["cost"] += costRUB 1895 bySect[statData["sector"]]["percent"] += percentCostRUB 1896 1897 # adding distribution by unique currencies: 1898 if currency not in byCurr.keys(): 1899 byCurr[currency] = { 1900 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1901 "cost": costRUB, 1902 "percent": percentCostRUB 1903 } 1904 1905 else: 1906 byCurr[currency]["cost"] += costRUB 1907 byCurr[currency]["percent"] += percentCostRUB 1908 1909 # saving statistics for every instrument: 1910 if item["instrumentType"] == "currency": 1911 view["stat"]["Currencies"].append(statData) 1912 1913 # update dict with free funds for trading (total - blocked) by currencies 1914 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1915 view["stat"]["funds"][currency] = { 1916 "total": volume, 1917 "totalCostRUB": costRUB, # total volume cost in rubles 1918 "free": volume - blocked, 1919 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1920 } 1921 1922 elif item["instrumentType"] == "share": 1923 view["stat"]["Shares"].append(statData) 1924 1925 elif item["instrumentType"] == "bond": 1926 view["stat"]["Bonds"].append(statData) 1927 1928 elif item["instrumentType"] == "etf": 1929 view["stat"]["Etfs"].append(statData) 1930 1931 elif item["instrumentType"] == "Futures": 1932 view["stat"]["Futures"].append(statData) 1933 1934 else: 1935 continue 1936 1937 # total changes in Russian Ruble: 1938 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1939 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1940 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1941 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1942 view["stat"]["funds"]["rub"] = { 1943 "total": view["stat"]["availableRUB"], 1944 "totalCostRUB": view["stat"]["availableRUB"], 1945 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1946 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1947 } 1948 1949 # --- pending limit orders sector data: 1950 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1951 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1952 1953 for item in view["raw"]["orders"]: 1954 self._figi = item["figi"] 1955 1956 if item["figi"] not in uniquePendingOrdersFIGIs: 1957 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1958 1959 uniquePendingOrdersFIGIs.append(item["figi"]) 1960 uniquePendingOrders[item["figi"]] = instrument 1961 1962 else: 1963 instrument = uniquePendingOrders[item["figi"]] 1964 1965 if instrument: 1966 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1967 orderType = TKS_ORDER_TYPES[item["orderType"]] 1968 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1969 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1970 1971 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1972 if item["direction"] == "ORDER_DIRECTION_BUY": 1973 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1974 1975 else: 1976 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1977 1978 # requested price for order execution: 1979 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1980 1981 # necessary changes in percent to reach target from current price: 1982 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1983 1984 view["stat"]["orders"].append({ 1985 "orderID": item["orderId"], # orderId number parameter of current order 1986 "figi": item["figi"], # FIGI identification 1987 "ticker": instrument["ticker"], # ticker name by FIGI 1988 "lotsRequested": item["lotsRequested"], # requested lots value 1989 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1990 "currentPrice": lastPrice, # current instrument's price for defined action 1991 "targetPrice": target, # requested price for order execution in base currency 1992 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1993 "percentChanges": changes, # changes in percent to target from current price 1994 "currency": item["currency"], # instrument's currency name 1995 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1996 "type": orderType, # type of order from TKS_ORDER_TYPES 1997 "status": orderState, # order status from TKS_ORDER_STATES 1998 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1999 }) 2000 2001 # --- stop orders sector data: 2002 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 2003 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 2004 2005 for item in view["raw"]["stopOrders"]: 2006 self._figi = item["figi"] 2007 2008 if item["figi"] not in uniqueStopOrdersFIGIs: 2009 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 2010 2011 uniqueStopOrdersFIGIs.append(item["figi"]) 2012 uniqueStopOrders[item["figi"]] = instrument 2013 2014 else: 2015 instrument = uniqueStopOrders[item["figi"]] 2016 2017 if instrument: 2018 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 2019 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 2020 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 2021 2022 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 2023 if "expirationTime" in item.keys(): 2024 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 2025 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 2026 2027 else: 2028 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 2029 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 2030 2031 # current instrument's price (last sellers order if buy, and last buyers order if sell): 2032 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 2033 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2034 2035 else: 2036 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2037 2038 # requested price when stop-order executed: 2039 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2040 2041 # price for limit-order, set up when stop-order executed: 2042 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2043 2044 # necessary changes in percent to reach target from current price: 2045 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2046 2047 view["stat"]["stopOrders"].append({ 2048 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2049 "figi": item["figi"], # FIGI identification 2050 "ticker": instrument["ticker"], # ticker name by FIGI 2051 "lotsRequested": item["lotsRequested"], # requested lots value 2052 "currentPrice": lastPrice, # current instrument's price for defined action 2053 "targetPrice": target, # requested price for stop-order execution in base currency 2054 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2055 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2056 "percentChanges": changes, # changes in percent to target from current price 2057 "currency": item["currency"], # instrument's currency name 2058 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2059 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2060 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2061 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2062 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2063 }) 2064 2065 # --- calculating data for analytics section: 2066 # portfolio distribution by assets: 2067 view["analytics"]["distrByAssets"] = { 2068 "Ruble": { 2069 "uniques": 1, 2070 "cost": view["stat"]["availableRUB"], 2071 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2072 }, 2073 "Currencies": { 2074 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2075 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2076 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2077 }, 2078 "Shares": { 2079 "uniques": len(view["stat"]["Shares"]), 2080 "cost": view["stat"]["sharesCostRUB"], 2081 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2082 }, 2083 "Bonds": { 2084 "uniques": len(view["stat"]["Bonds"]), 2085 "cost": view["stat"]["bondsCostRUB"], 2086 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2087 }, 2088 "Etfs": { 2089 "uniques": len(view["stat"]["Etfs"]), 2090 "cost": view["stat"]["etfsCostRUB"], 2091 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2092 }, 2093 "Futures": { 2094 "uniques": len(view["stat"]["Futures"]), 2095 "cost": view["stat"]["futuresCostRUB"], 2096 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2097 }, 2098 } 2099 2100 # portfolio distribution by companies: 2101 view["analytics"]["distrByCompanies"]["All money cash"] = { 2102 "ticker": "", 2103 "cost": view["stat"]["allCurrenciesCostRUB"], 2104 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2105 } 2106 view["analytics"]["distrByCompanies"].update(byComp) 2107 2108 # portfolio distribution by sectors: 2109 view["analytics"]["distrBySectors"]["All money cash"] = { 2110 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2111 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2112 } 2113 view["analytics"]["distrBySectors"].update(bySect) 2114 2115 # portfolio distribution by currencies: 2116 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2117 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2118 2119 if self.moreDebug: 2120 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2121 2122 view["analytics"]["distrByCurrencies"].update(byCurr) 2123 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2124 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2125 2126 # portfolio distribution by countries: 2127 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2128 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2129 2130 if self.moreDebug: 2131 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2132 2133 view["analytics"]["distrByCountries"].update(byCountry) 2134 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2135 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2136 2137 # --- Prepare text statistics overview in human-readable: 2138 if show or onlyFiles: 2139 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2140 2141 # Whatever the value `details`, header not changes: 2142 info = [ 2143 "# Client's portfolio\n\n", 2144 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2145 "* **Account ID:** [{}]\n".format(self.accountId), 2146 ] 2147 2148 if details in ["full", "positions", "digest"]: 2149 info.extend([ 2150 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2151 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2152 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2153 view["stat"]["totalChangesRUB"], 2154 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2155 view["stat"]["totalChangesPercentRUB"], 2156 ), 2157 ]) 2158 2159 if details in ["full", "positions"]: 2160 info.extend([ 2161 "## Open positions\n\n", 2162 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2163 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2164 "| **Ruble:** | {:>31} | | | | | |\n".format( 2165 "{:.2f} ({:.2f}) rub".format( 2166 view["stat"]["availableRUB"], 2167 view["stat"]["blockedRUB"], 2168 ) 2169 ) 2170 ]) 2171 2172 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2173 return [ 2174 "| | | | | | | |\n", 2175 "| {:<27} | | | | | {:>19} | |\n".format( 2176 noTradeStr if noTradeStr else typeStr, 2177 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2178 ), 2179 ] 2180 2181 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2182 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2183 "{} [{}]".format(data["ticker"], data["figi"]), 2184 "{:.2f} ({:.2f}) {}".format( 2185 data["volume"], 2186 data["blocked"], 2187 data["currency"], 2188 ) if isCurr else "{:.0f} ({:.0f})".format( 2189 data["volume"], 2190 data["blocked"], 2191 ), 2192 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2193 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2194 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2195 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2196 "{}{:.2f} {} ({}{:.2f}%)".format( 2197 "+" if data["profit"] > 0 else "", 2198 data["profit"], data["baseCurrencyName"], 2199 "+" if data["percentProfit"] > 0 else "", 2200 data["percentProfit"], 2201 ), 2202 ) 2203 2204 # --- Show currencies section: 2205 if view["stat"]["Currencies"]: 2206 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2207 for item in view["stat"]["Currencies"]: 2208 info.append(_InfoStr(item, isCurr=True)) 2209 2210 else: 2211 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2212 2213 # --- Show shares section: 2214 if view["stat"]["Shares"]: 2215 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2216 2217 for item in view["stat"]["Shares"]: 2218 info.append(_InfoStr(item)) 2219 2220 else: 2221 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2222 2223 # --- Show bonds section: 2224 if view["stat"]["Bonds"]: 2225 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2226 2227 for item in view["stat"]["Bonds"]: 2228 info.append(_InfoStr(item)) 2229 2230 else: 2231 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2232 2233 # --- Show etfs section: 2234 if view["stat"]["Etfs"]: 2235 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2236 2237 for item in view["stat"]["Etfs"]: 2238 info.append(_InfoStr(item)) 2239 2240 else: 2241 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2242 2243 # --- Show futures section: 2244 if view["stat"]["Futures"]: 2245 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2246 2247 for item in view["stat"]["Futures"]: 2248 info.append(_InfoStr(item)) 2249 2250 else: 2251 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2252 2253 if details in ["full", "orders"]: 2254 # --- Show pending limit orders section: 2255 if view["stat"]["orders"]: 2256 info.extend([ 2257 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2258 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2259 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2260 ]) 2261 2262 for item in view["stat"]["orders"]: 2263 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2264 "{} [{}]".format(item["ticker"], item["figi"]), 2265 item["orderID"], 2266 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2267 "{} {} ({}{:.2f}%)".format( 2268 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2269 item["baseCurrencyName"], 2270 "+" if item["percentChanges"] > 0 else "", 2271 float(item["percentChanges"]), 2272 ), 2273 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2274 item["action"], 2275 item["type"], 2276 item["date"], 2277 )) 2278 2279 else: 2280 info.append("\n## Total pending limit-orders: [0]\n") 2281 2282 # --- Show stop orders section: 2283 if view["stat"]["stopOrders"]: 2284 info.extend([ 2285 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2286 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2287 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2288 ]) 2289 2290 for item in view["stat"]["stopOrders"]: 2291 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2292 "{} [{}]".format(item["ticker"], item["figi"]), 2293 item["orderID"], 2294 item["lotsRequested"], 2295 "{} {} ({}{:.2f}%)".format( 2296 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2297 item["baseCurrencyName"], 2298 "+" if item["percentChanges"] > 0 else "", 2299 float(item["percentChanges"]), 2300 ), 2301 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2302 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2303 item["action"], 2304 item["type"], 2305 item["expType"], 2306 item["createDate"], 2307 item["expDate"], 2308 )) 2309 2310 else: 2311 info.append("\n## Total stop-orders: [0]\n") 2312 2313 if details in ["full", "analytics"]: 2314 # -- Show analytics section: 2315 if view["stat"]["portfolioCostRUB"] > 0: 2316 info.extend([ 2317 "\n# Analytics\n\n" 2318 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2319 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2320 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2321 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2322 view["stat"]["totalChangesRUB"], 2323 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2324 view["stat"]["totalChangesPercentRUB"], 2325 ), 2326 "\n## Portfolio distribution by assets\n" 2327 "\n| Type | Uniques | Percent | Current cost |\n", 2328 "|------------------------------------|---------|---------|--------------------|\n", 2329 ]) 2330 2331 for key in view["analytics"]["distrByAssets"].keys(): 2332 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2333 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2334 key, 2335 view["analytics"]["distrByAssets"][key]["uniques"], 2336 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2337 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2338 )) 2339 2340 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2341 2342 info.extend([ 2343 "\n## Portfolio distribution by companies\n" 2344 "\n| Company | Percent | Current cost |\n", 2345 aSepLine, 2346 ]) 2347 2348 for company in view["analytics"]["distrByCompanies"].keys(): 2349 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2350 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2351 "{}{}".format( 2352 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2353 company, 2354 ), 2355 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2356 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2357 )) 2358 2359 info.extend([ 2360 "\n## Portfolio distribution by sectors\n" 2361 "\n| Sector | Percent | Current cost |\n", 2362 aSepLine, 2363 ]) 2364 2365 for sector in view["analytics"]["distrBySectors"].keys(): 2366 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2367 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2368 sector, 2369 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2370 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2371 )) 2372 2373 info.extend([ 2374 "\n## Portfolio distribution by currencies\n" 2375 "\n| Instruments currencies | Percent | Current cost |\n", 2376 aSepLine, 2377 ]) 2378 2379 for curr in view["analytics"]["distrByCurrencies"].keys(): 2380 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2381 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2382 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2383 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2384 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2385 )) 2386 2387 info.extend([ 2388 "\n## Portfolio distribution by countries\n" 2389 "\n| Assets by country | Percent | Current cost |\n", 2390 aSepLine, 2391 ]) 2392 2393 for country in view["analytics"]["distrByCountries"].keys(): 2394 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2395 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2396 country, 2397 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2398 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2399 )) 2400 2401 if details in ["full", "calendar"]: 2402 # -- Show bonds payment calendar section: 2403 if view["stat"]["Bonds"]: 2404 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2405 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2406 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2407 2408 else: 2409 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2410 2411 infoText = "".join(info) 2412 2413 if show and not onlyFiles: 2414 uLogger.info(infoText) 2415 2416 if details == "full" and self.overviewFile: 2417 filename = self.overviewFile 2418 2419 elif details == "digest" and self.overviewDigestFile: 2420 filename = self.overviewDigestFile 2421 2422 elif details == "positions" and self.overviewPositionsFile: 2423 filename = self.overviewPositionsFile 2424 2425 elif details == "orders" and self.overviewOrdersFile: 2426 filename = self.overviewOrdersFile 2427 2428 elif details == "analytics" and self.overviewAnalyticsFile: 2429 filename = self.overviewAnalyticsFile 2430 2431 elif details == "calendar" and self.overviewBondsCalendarFile: 2432 filename = self.overviewBondsCalendarFile 2433 2434 else: 2435 filename = "" 2436 2437 if filename and (show or onlyFiles): 2438 with open(filename, "w", encoding="UTF-8") as fH: 2439 fH.write(infoText) 2440 2441 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2442 2443 if self.useHTMLReports: 2444 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2445 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2446 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2447 2448 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2449 2450 return view 2451 2452 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]: 2453 """ 2454 Returns history operations between two given dates for current `accountId`. 2455 If `reportFile` string is not empty then also save human-readable report. 2456 Shows some statistical data of closed positions. 2457 2458 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2459 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2460 :param show: if `True` then also prints all records to the console. 2461 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2462 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2463 :return: original list of dictionaries with history of deals records from API ("operations" key): 2464 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2465 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2466 """ 2467 if self.accountId is None or not self.accountId: 2468 uLogger.error("Variable `accountId` must be defined for using this method!") 2469 raise Exception("Account ID required") 2470 2471 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2472 2473 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2474 2475 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2476 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2477 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2478 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2479 customStat = {} # custom statistics in additional to responseJSON 2480 2481 # --- output report in human-readable format: 2482 if self.reportFile and (show or onlyFiles): 2483 splitLine1 = "| | | | | |\n" # Summary section 2484 splitLine2 = "| | | | | | | | |\n" # Operations section 2485 nextDay = "" 2486 2487 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2488 2489 if len(ops) > 0: 2490 customStat = { 2491 "opsCount": 0, # total operations count 2492 "buyCount": 0, # buy operations 2493 "sellCount": 0, # sell operations 2494 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2495 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2496 "payIn": {"rub": 0.}, # Deposit brokerage account 2497 "payOut": {"rub": 0.}, # Withdrawals 2498 "divs": {"rub": 0.}, # Dividends income 2499 "coupons": {"rub": 0.}, # Coupon's income 2500 "brokerCom": {"rub": 0.}, # Service commissions 2501 "serviceCom": {"rub": 0.}, # Service commissions 2502 "marginCom": {"rub": 0.}, # Margin commissions 2503 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2504 } 2505 2506 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2507 for item in ops: 2508 if item["state"] == "OPERATION_STATE_EXECUTED": 2509 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2510 2511 # count buy operations: 2512 if "_BUY" in item["operationType"]: 2513 customStat["buyCount"] += 1 2514 2515 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2516 customStat["buyTotal"][item["payment"]["currency"]] += payment 2517 2518 else: 2519 customStat["buyTotal"][item["payment"]["currency"]] = payment 2520 2521 # count sell operations: 2522 elif "_SELL" in item["operationType"]: 2523 customStat["sellCount"] += 1 2524 2525 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2526 customStat["sellTotal"][item["payment"]["currency"]] += payment 2527 2528 else: 2529 customStat["sellTotal"][item["payment"]["currency"]] = payment 2530 2531 # count incoming operations: 2532 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2533 if item["payment"]["currency"] in customStat["payIn"].keys(): 2534 customStat["payIn"][item["payment"]["currency"]] += payment 2535 2536 else: 2537 customStat["payIn"][item["payment"]["currency"]] = payment 2538 2539 # count withdrawals operations: 2540 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2541 if item["payment"]["currency"] in customStat["payOut"].keys(): 2542 customStat["payOut"][item["payment"]["currency"]] += payment 2543 2544 else: 2545 customStat["payOut"][item["payment"]["currency"]] = payment 2546 2547 # count dividends income: 2548 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2549 if item["payment"]["currency"] in customStat["divs"].keys(): 2550 customStat["divs"][item["payment"]["currency"]] += payment 2551 2552 else: 2553 customStat["divs"][item["payment"]["currency"]] = payment 2554 2555 # count coupon's income: 2556 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2557 if item["payment"]["currency"] in customStat["coupons"].keys(): 2558 customStat["coupons"][item["payment"]["currency"]] += payment 2559 2560 else: 2561 customStat["coupons"][item["payment"]["currency"]] = payment 2562 2563 # count broker commissions: 2564 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2565 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2566 customStat["brokerCom"][item["payment"]["currency"]] += payment 2567 2568 else: 2569 customStat["brokerCom"][item["payment"]["currency"]] = payment 2570 2571 # count service commissions: 2572 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2573 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2574 customStat["serviceCom"][item["payment"]["currency"]] += payment 2575 2576 else: 2577 customStat["serviceCom"][item["payment"]["currency"]] = payment 2578 2579 # count margin commissions: 2580 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2581 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2582 customStat["marginCom"][item["payment"]["currency"]] += payment 2583 2584 else: 2585 customStat["marginCom"][item["payment"]["currency"]] = payment 2586 2587 # count withholding taxes: 2588 elif "_TAX" in item["operationType"]: 2589 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2590 customStat["allTaxes"][item["payment"]["currency"]] += payment 2591 2592 else: 2593 customStat["allTaxes"][item["payment"]["currency"]] = payment 2594 2595 else: 2596 continue 2597 2598 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2599 2600 # --- view "Actions" lines: 2601 info.extend([ 2602 "| Report sections | | | | |\n", 2603 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2604 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2605 "| | Buy: {:<22} | {:<28} | | |\n".format( 2606 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2607 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2608 ), 2609 "| | Sell: {:<21} | {:<28} | | |\n".format( 2610 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2611 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2612 ), 2613 ]) 2614 2615 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2616 for key in opsKeys: 2617 if key == "rub": 2618 continue 2619 2620 info.extend([ 2621 "| | | {:<28} | | |\n".format( 2622 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2623 ), 2624 "| | | {:<28} | | |\n".format( 2625 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2626 ), 2627 ]) 2628 2629 info.append(splitLine1) 2630 2631 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2632 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2633 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2634 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2635 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2636 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2637 ) 2638 2639 # --- view "Payments" lines: 2640 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2641 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2642 2643 for key in paymentsKeys: 2644 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2645 2646 info.append(splitLine1) 2647 2648 # --- view "Commissions and taxes" lines: 2649 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2650 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2651 2652 for key in comKeys: 2653 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2654 2655 info.extend([ 2656 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2657 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2658 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2659 ]) 2660 2661 else: 2662 info.append("Broker returned no operations during this period\n") 2663 2664 # --- view "Operations" section: 2665 for item in ops: 2666 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2667 continue 2668 2669 else: 2670 self._figi = item["figi"] 2671 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2672 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2673 2674 # group of deals during one day: 2675 if nextDay and item["date"].split("T")[0] != nextDay: 2676 info.append(splitLine2) 2677 nextDay = "" 2678 2679 else: 2680 nextDay = item["date"].split("T")[0] # saving current day for splitting 2681 2682 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2683 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2684 self._figi if self._figi else "—", 2685 instrument["ticker"] if instrument else "—", 2686 instrument["type"] if instrument else "—", 2687 item["quantity"] if int(item["quantity"]) > 0 else "—", 2688 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2689 TKS_OPERATION_STATES[item["state"]], 2690 TKS_OPERATION_TYPES[item["operationType"]], 2691 )) 2692 2693 infoText = "".join(info) 2694 2695 if show and not onlyFiles: 2696 if self.moreDebug: 2697 uLogger.debug("Records about history of a client's operations successfully received") 2698 2699 uLogger.info(infoText) 2700 2701 if self.reportFile and (show or onlyFiles): 2702 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2703 fH.write(infoText) 2704 2705 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2706 2707 if self.useHTMLReports: 2708 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2709 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2710 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2711 2712 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2713 2714 return ops, customStat 2715 2716 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame: 2717 """ 2718 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2719 2720 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2721 Warning! Broker server used ISO UTC time by default. 2722 2723 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2724 Also, `historyFile` used to update history with `onlyMissing` parameter. 2725 2726 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2727 2728 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2729 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2730 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2731 `"hour"`, `"day"`. Default: `"hour"`. 2732 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2733 False by default. Warning! History appends only from last candle to current time 2734 with always update last candle! 2735 :param csvSep: separator if csv-file is used, `,` by default. 2736 :param show: if `True` then also prints Pandas DataFrame to the console. 2737 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2738 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2739 `["date", "time", "open", "high", "low", "close", "volume"]`. 2740 """ 2741 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2742 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2743 history = None # empty pandas object for history 2744 2745 if interval not in TKS_CANDLE_INTERVALS.keys(): 2746 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2747 raise Exception("Incorrect value") 2748 2749 if not (self._ticker or self._figi): 2750 uLogger.error("Ticker or FIGI must be defined!") 2751 raise Exception("Ticker or FIGI required") 2752 2753 if self._ticker and not self._figi: 2754 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2755 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2756 2757 if self._figi and not self._ticker: 2758 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2759 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2760 2761 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2762 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2763 if interval.lower() != "day": 2764 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2765 2766 delta = dtEnd - dtStart # current UTC time minus last time in file 2767 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2768 2769 # calculate history length in candles: 2770 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2771 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2772 length += 1 # to avoid fraction time 2773 2774 # calculate data blocks count: 2775 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2776 2777 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2778 if self.moreDebug: 2779 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2780 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2781 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2782 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2783 2784 tempOld = None # pandas object for old history, if --only-missing key present 2785 lastTime = None # datetime object of last old candle in file 2786 2787 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2788 if self.moreDebug: 2789 uLogger.debug("--only-missing key present, add only last missing candles...") 2790 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2791 2792 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2793 2794 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2795 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2796 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2797 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2798 2799 # get last datetime object from last string in file or minus 1 delta if file is empty: 2800 if len(tempOld) > 0: 2801 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2802 2803 else: 2804 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2805 2806 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2807 2808 responseJSONs = [] # raw history blocks of data 2809 2810 blockEnd = dtEnd 2811 for item in range(blocks): 2812 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2813 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2814 2815 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2816 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2817 )) 2818 2819 if blockStart == blockEnd: 2820 uLogger.debug("Skipped this zero-length block...") 2821 2822 else: 2823 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2824 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2825 self.body = str({ 2826 "figi": self._figi, 2827 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2828 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2829 "interval": TKS_CANDLE_INTERVALS[interval][0] 2830 }) 2831 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2832 2833 if "code" in responseJSON.keys(): 2834 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2835 2836 else: 2837 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2838 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2839 2840 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2841 2842 blockEnd = blockStart 2843 2844 printCount = len(responseJSONs) # candles to show in console 2845 if responseJSONs: 2846 tempHistory = pd.DataFrame( 2847 data={ 2848 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2849 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2850 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2851 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2852 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2853 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2854 "volume": [int(item["volume"]) for item in responseJSONs], 2855 }, 2856 index=range(len(responseJSONs)), 2857 columns=["date", "time", "open", "high", "low", "close", "volume"], 2858 ) 2859 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2860 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2861 2862 # append only newest candles to old history if --only-missing key present: 2863 if onlyMissing and tempOld is not None and lastTime is not None: 2864 index = 0 # find start index in tempHistory data: 2865 2866 for i, item in tempHistory.iterrows(): 2867 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2868 2869 if curTime == lastTime: 2870 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2871 index = i 2872 printCount = index + 1 2873 break 2874 2875 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2876 2877 else: 2878 history = tempHistory # if no `--only-missing` key then load full data from server 2879 2880 if self.moreDebug: 2881 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2882 2883 if history is not None and not history.empty: 2884 if show and not onlyFiles: 2885 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2886 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2887 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2888 )) 2889 2890 else: 2891 uLogger.warning("Received an empty candles history!") 2892 2893 if self.historyFile is not None: 2894 if history is not None and not history.empty: 2895 history.to_csv(self.historyFile, sep=csvSep, index=False, header=False) 2896 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2897 2898 else: 2899 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2900 2901 else: 2902 if self.moreDebug: 2903 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2904 2905 return history 2906 2907 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2908 """ 2909 Load candles history from csv-file and return Pandas DataFrame object. 2910 2911 See also: `History()` and `ShowHistoryChart()` methods. 2912 2913 :param filePath: path to csv-file to open. 2914 """ 2915 loadedHistory = None # init candles data object 2916 2917 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2918 2919 if os.path.exists(filePath): 2920 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2921 2922 tfStr = self.priceModel.FormattedDelta( 2923 self.priceModel.timeframe, 2924 "{days} days {hours}h {minutes}m {seconds}s", 2925 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2926 self.priceModel.timeframe, 2927 "{hours}h {minutes}m {seconds}s", 2928 ) 2929 2930 if loadedHistory is not None and not loadedHistory.empty: 2931 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2932 len(loadedHistory), 2933 tfStr, 2934 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2935 ) 2936 2937 else: 2938 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2939 2940 else: 2941 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2942 2943 return loadedHistory 2944 2945 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2946 """ 2947 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2948 2949 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2950 Default: `index.html` (both for interact and non-interact candlesticks chart). 2951 2952 See also: `History()` and `LoadHistory()` methods. 2953 2954 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2955 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2956 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2957 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2958 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2959 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2960 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2961 """ 2962 if isinstance(candles, str): 2963 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2964 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2965 2966 elif isinstance(candles, pd.DataFrame): 2967 self.priceModel.prices = candles # set candles chain from variable 2968 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2969 2970 if "datetime" not in candles.columns: 2971 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2972 2973 else: 2974 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2975 raise Exception("Incorrect value") 2976 2977 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2978 2979 if interact: 2980 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2981 2982 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2983 2984 else: 2985 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2986 2987 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2988 2989 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2990 2991 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2992 """ 2993 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2994 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2995 2996 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2997 2998 :param operation: string "Buy" or "Sell". 2999 :param lots: volume, integer count of lots >= 1. 3000 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 3001 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 3002 :param expDate: string "Undefined" by default or local date in future, 3003 it is a string with format `%Y-%m-%d %H:%M:%S`. 3004 :return: JSON with response from broker server. 3005 """ 3006 if self.accountId is None or not self.accountId: 3007 uLogger.error("Variable `accountId` must be defined for using this method!") 3008 raise Exception("Account ID required") 3009 3010 if operation is None or not operation or operation not in ("Buy", "Sell"): 3011 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3012 raise Exception("Incorrect value") 3013 3014 if lots is None or lots < 1: 3015 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 3016 lots = 1 3017 3018 if tp is None or tp < 0: 3019 tp = 0 3020 3021 if sl is None or sl < 0: 3022 sl = 0 3023 3024 if expDate is None or not expDate: 3025 expDate = "Undefined" 3026 3027 if not (self._ticker or self._figi): 3028 uLogger.error("Ticker or FIGI must be defined!") 3029 raise Exception("Ticker or FIGI required") 3030 3031 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3032 self._ticker = instrument["ticker"] 3033 self._figi = instrument["figi"] 3034 3035 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 3036 3037 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3038 self.body = str({ 3039 "figi": self._figi, 3040 "quantity": str(lots), 3041 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3042 "accountId": str(self.accountId), 3043 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3044 }) 3045 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3046 3047 if "orderId" in response.keys(): 3048 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3049 operation, response["orderId"], 3050 self._ticker, self._figi, lots, 3051 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3052 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3053 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3054 )) 3055 3056 if tp > 0: 3057 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3058 3059 if sl > 0: 3060 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3061 3062 else: 3063 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3064 3065 return response 3066 3067 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3068 """ 3069 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3070 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3071 3072 See also: `Order()` and `Trade()` docstrings. 3073 3074 :param lots: volume, integer count of lots >= 1. 3075 :param tp: float > 0, take profit price of stop-order. 3076 :param sl: float > 0, stop loss price of stop-order. 3077 :param expDate: it's a local date in future. 3078 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3079 :return: JSON with response from broker server. 3080 """ 3081 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3082 3083 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3084 """ 3085 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3086 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3087 3088 See also: `Order()` and `Trade()` docstrings. 3089 3090 :param lots: volume, integer count of lots >= 1. 3091 :param tp: float > 0, take profit price of stop-order. 3092 :param sl: float > 0, stop loss price of stop-order. 3093 :param expDate: it's a local date in the future. 3094 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3095 :return: JSON with response from broker server. 3096 """ 3097 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3098 3099 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3100 """ 3101 Close position of given instruments. 3102 3103 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3104 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3105 This avoids unnecessary downloading data from the server. 3106 """ 3107 if instruments is None or not instruments: 3108 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3109 raise Exception("Ticker or FIGI required") 3110 3111 if isinstance(instruments, str): 3112 instruments = [instruments] 3113 3114 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3115 if uniqueInstruments: 3116 if portfolio is None or not portfolio: 3117 portfolio = self.Overview(show=False) 3118 3119 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3120 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3121 3122 for self._figi in uniqueInstruments: 3123 if self._figi not in allOpened: 3124 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3125 continue 3126 3127 # search open trade info about instrument by ticker: 3128 instrument = {} 3129 for iType in TKS_INSTRUMENTS: 3130 if instrument: 3131 break 3132 3133 for item in portfolio["stat"][iType]: 3134 if item["figi"] == self._figi: 3135 instrument = item 3136 break 3137 3138 if instrument: 3139 self._ticker = instrument["ticker"] 3140 self._figi = instrument["figi"] 3141 3142 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3143 self._ticker, 3144 self._figi, 3145 int(instrument["volume"]), 3146 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3147 )) 3148 3149 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3150 3151 if tradeLots > 0: 3152 if instrument["blocked"] > 0: 3153 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3154 instrument["blocked"], 3155 self._ticker, 3156 tradeLots, 3157 )) 3158 3159 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3160 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3161 3162 else: 3163 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3164 3165 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3166 """ 3167 Close all positions of given instruments with defined type. 3168 3169 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3170 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3171 This avoids unnecessary downloading data from the server. 3172 """ 3173 if iType not in TKS_INSTRUMENTS: 3174 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3175 3176 else: 3177 if portfolio is None or not portfolio: 3178 portfolio = self.Overview(show=False) 3179 3180 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3181 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3182 3183 if tickers and portfolio: 3184 self.CloseTrades(tickers, portfolio) 3185 3186 else: 3187 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3188 3189 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3190 """ 3191 Universal method to create market or limit orders with all available parameters for current `accountId`. 3192 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3193 3194 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3195 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3196 3197 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3198 then broker immediately open market order as you can do simple --buy or --sell operations! 3199 3200 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3201 When current price will go up or down to target price value then broker opens a limit order. 3202 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3203 3204 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3205 3206 :param operation: string "Buy" or "Sell". 3207 :param orderType: string "Limit" or "Stop". 3208 :param lots: volume, integer count of lots >= 1. 3209 :param targetPrice: target price > 0. This is open trade price for limit order. 3210 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3211 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3212 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3213 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3214 Stop loss order always executed by market price. 3215 :param expDate: string "Undefined" by default or local date in future. 3216 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3217 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3218 A limit order has no expiration date, it lasts until the end of the trading day. 3219 :return: JSON with response from broker server. 3220 """ 3221 if self.accountId is None or not self.accountId: 3222 uLogger.error("Variable `accountId` must be defined for using this method!") 3223 raise Exception("Account ID required") 3224 3225 if operation is None or not operation or operation not in ("Buy", "Sell"): 3226 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3227 raise Exception("Incorrect value") 3228 3229 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3230 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3231 raise Exception("Incorrect value") 3232 3233 if lots is None or lots < 1: 3234 uLogger.error("You must define trade volume > 0: integer count of lots!") 3235 raise Exception("Incorrect value") 3236 3237 if targetPrice is None or targetPrice <= 0: 3238 uLogger.error("Target price for limit-order must be greater than 0!") 3239 raise Exception("Incorrect value") 3240 3241 if limitPrice is None or limitPrice <= 0: 3242 limitPrice = targetPrice 3243 3244 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3245 stopType = "Limit" 3246 3247 if expDate is None or not expDate: 3248 expDate = "Undefined" 3249 3250 if not (self._ticker or self._figi): 3251 uLogger.error("Tocker or FIGI must be defined!") 3252 raise Exception("Ticker or FIGI required") 3253 3254 response = {} 3255 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3256 self._ticker = instrument["ticker"] 3257 self._figi = instrument["figi"] 3258 3259 if orderType == "Limit": 3260 uLogger.debug( 3261 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3262 self._ticker, self._figi, 3263 operation, lots, targetPrice, instrument["currency"], 3264 )) 3265 3266 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3267 self.body = str({ 3268 "figi": self._figi, 3269 "quantity": str(lots), 3270 "price": FloatToNano(targetPrice), 3271 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3272 "accountId": str(self.accountId), 3273 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3274 }) 3275 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3276 3277 if "orderId" in response.keys(): 3278 uLogger.info( 3279 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3280 response["orderId"], self._ticker, self._figi, operation, lots, 3281 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3282 )) 3283 3284 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3285 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3286 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3287 targetPrice, instrument["currency"], 3288 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3289 )) 3290 3291 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3292 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3293 targetPrice, instrument["currency"], 3294 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3295 )) 3296 3297 else: 3298 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3299 3300 if orderType == "Stop": 3301 uLogger.debug( 3302 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3303 self._ticker, self._figi, 3304 operation, lots, 3305 targetPrice, instrument["currency"], 3306 limitPrice, instrument["currency"], 3307 stopType, expDate, 3308 )) 3309 3310 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3311 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3312 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3313 3314 body = { 3315 "figi": self._figi, 3316 "quantity": str(lots), 3317 "price": FloatToNano(limitPrice), 3318 "stopPrice": FloatToNano(targetPrice), 3319 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3320 "accountId": str(self.accountId), 3321 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3322 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3323 } 3324 3325 if expDateUTC: 3326 body["expireDate"] = expDateUTC 3327 3328 self.body = str(body) 3329 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3330 3331 if "stopOrderId" in response.keys(): 3332 uLogger.info( 3333 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3334 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3335 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3336 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3337 TKS_STOP_ORDER_TYPES[stopOrderType], 3338 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3339 )) 3340 3341 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3342 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3343 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{} {}] is lower than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3344 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3345 "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"], 3346 )) 3347 3348 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3349 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{} {}] is higher than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3350 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3351 "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"], 3352 )) 3353 3354 else: 3355 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3356 3357 return response 3358 3359 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3360 """ 3361 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3362 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3363 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3364 See also: `Order()` docstring. 3365 3366 :param lots: volume, integer count of lots >= 1. 3367 :param targetPrice: target price > 0. This is open trade price for limit order. 3368 :return: JSON with response from broker server. 3369 """ 3370 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3371 3372 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3373 """ 3374 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3375 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3376 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3377 target price value then broker opens a limit order. See also: `Order()` docstring. 3378 3379 :param lots: volume, integer count of lots >= 1. 3380 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3381 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3382 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3383 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3384 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3385 :param expDate: string "Undefined" by default or local date in future. 3386 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3387 This date is converting to UTC format for server. 3388 :return: JSON with response from broker server. 3389 """ 3390 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3391 3392 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3393 """ 3394 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3395 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3396 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3397 See also: `Order()` docstring. 3398 3399 :param lots: volume, integer count of lots >= 1. 3400 :param targetPrice: target price > 0. This is open trade price for limit order. 3401 :return: JSON with response from broker server. 3402 """ 3403 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3404 3405 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3406 """ 3407 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3408 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3409 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3410 target price value then broker opens a limit order. See also: `Order()` docstring. 3411 3412 :param lots: volume, integer count of lots >= 1. 3413 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3414 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3415 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3416 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3417 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3418 :param expDate: string "Undefined" by default or local date in future. 3419 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3420 This date is converting to UTC format for server. 3421 :return: JSON with response from broker server. 3422 """ 3423 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3424 3425 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3426 """ 3427 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3428 3429 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3430 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3431 This avoids unnecessary downloading data from the server. 3432 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3433 """ 3434 if self.accountId is None or not self.accountId: 3435 uLogger.error("Variable `accountId` must be defined for using this method!") 3436 raise Exception("Account ID required") 3437 3438 if orderIDs: 3439 if allOrdersIDs is None: 3440 rawOrders = self.RequestPendingOrders() 3441 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3442 3443 if allStopOrdersIDs is None: 3444 rawStopOrders = self.RequestStopOrders() 3445 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3446 3447 for orderID in orderIDs: 3448 idInPendingOrders = orderID in allOrdersIDs 3449 idInStopOrders = orderID in allStopOrdersIDs 3450 3451 if not (idInPendingOrders or idInStopOrders): 3452 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3453 continue 3454 3455 else: 3456 if idInPendingOrders: 3457 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3458 3459 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3460 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3461 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3462 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3463 3464 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3465 if self.moreDebug: 3466 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3467 3468 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3469 3470 else: 3471 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3472 3473 elif idInStopOrders: 3474 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3475 3476 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3477 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3478 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3479 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3480 3481 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3482 if self.moreDebug: 3483 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3484 3485 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3486 3487 else: 3488 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3489 3490 else: 3491 continue 3492 3493 def CloseAllOrders(self) -> None: 3494 """ 3495 Gets a list of open pending and stop orders and cancel it all. 3496 """ 3497 rawOrders = self.RequestPendingOrders() 3498 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3499 lenOrders = len(allOrdersIDs) 3500 3501 rawStopOrders = self.RequestStopOrders() 3502 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3503 lenSOrders = len(allStopOrdersIDs) 3504 3505 if lenOrders > 0 or lenSOrders > 0: 3506 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3507 3508 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3509 3510 else: 3511 uLogger.info("Orders not found, nothing to cancel.") 3512 3513 def CloseAll(self, *args) -> None: 3514 """ 3515 Close all available (not blocked) opened trades and orders. 3516 3517 Also, you can select one or more keywords case-insensitive: 3518 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3519 3520 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3521 """ 3522 overview = self.Overview(show=False) # get all open trades info 3523 3524 if len(args) == 0: 3525 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3526 self.CloseAllOrders() # close all pending and stop orders 3527 3528 for iType in TKS_INSTRUMENTS: 3529 if iType != "Currencies": 3530 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3531 3532 else: 3533 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3534 lowerArgs = [x.lower() for x in args] 3535 3536 if "orders" in lowerArgs: 3537 self.CloseAllOrders() # close all pending and stop orders 3538 3539 for iType in TKS_INSTRUMENTS: 3540 if iType.lower() in lowerArgs and iType != "Currencies": 3541 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3542 3543 def CloseAllByTicker(self, instrument: str) -> None: 3544 """ 3545 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3546 3547 This method searches opened trade and orders of instrument throw all portfolio and then use 3548 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3549 3550 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3551 3552 :param instrument: string with ticker. 3553 """ 3554 if instrument is None or not instrument: 3555 uLogger.error("Ticker name must be defined for using this method!") 3556 raise Exception("Ticker required") 3557 3558 overview = self.Overview(show=False) # get user portfolio with all open trades info 3559 3560 self._ticker = instrument # try to set instrument as ticker 3561 self._figi = "" 3562 3563 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3564 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3565 3566 if limitAll and self.IsInLimitOrders(portfolio=overview): 3567 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3568 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3569 3570 if stopAll and self.IsInStopOrders(portfolio=overview): 3571 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3572 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3573 3574 if self.IsInPortfolio(portfolio=overview): 3575 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3576 self.CloseTrades(instruments=[instrument], portfolio=overview) 3577 3578 def CloseAllByFIGI(self, instrument: str) -> None: 3579 """ 3580 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3581 3582 This method searches opened trade and orders of instrument throw all portfolio and then use 3583 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3584 3585 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3586 3587 :param instrument: string with FIGI id. 3588 """ 3589 if instrument is None or not instrument: 3590 uLogger.error("FIGI id must be defined for using this method!") 3591 raise Exception("FIGI required") 3592 3593 overview = self.Overview(show=False) # get user portfolio with all open trades info 3594 3595 self._ticker = "" 3596 self._figi = instrument # try to set instrument as FIGI id 3597 3598 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3599 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3600 3601 if limitAll and self.IsInLimitOrders(portfolio=overview): 3602 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3603 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3604 3605 if stopAll and self.IsInStopOrders(portfolio=overview): 3606 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3607 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3608 3609 if self.IsInPortfolio(portfolio=overview): 3610 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3611 self.CloseTrades(instruments=[instrument], portfolio=overview) 3612 3613 @staticmethod 3614 def ParseOrderParameters(operation, **inputParameters): 3615 """ 3616 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3617 3618 :param operation: string "Buy" or "Sell". 3619 :param inputParameters: this is dict of strings that looks like this 3620 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3621 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3622 "prices" key: one or more prices to open limit-orders 3623 Counts of values in lots and prices lists must be equals! 3624 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3625 """ 3626 # TODO: update order grid work with api v2 3627 pass 3628 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3629 # 3630 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3631 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3632 # raise Exception("Incorrect value") 3633 # 3634 # if "l" in inputParameters.keys(): 3635 # inputParameters["lots"] = inputParameters.pop("l") 3636 # 3637 # if "p" in inputParameters.keys(): 3638 # inputParameters["prices"] = inputParameters.pop("p") 3639 # 3640 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3641 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3642 # raise Exception("Incorrect value") 3643 # 3644 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3645 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3646 # 3647 # if len(lots) != len(prices): 3648 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3649 # raise Exception("Incorrect value") 3650 # 3651 # uLogger.debug("Extracted parameters for orders:") 3652 # uLogger.debug("lots = {}".format(lots)) 3653 # uLogger.debug("prices = {}".format(prices)) 3654 # 3655 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3656 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3657 # uLogger.debug("Order parameters: {}".format(result)) 3658 # 3659 # return result 3660 3661 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3662 """ 3663 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3664 3665 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3666 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3667 """ 3668 result = False 3669 msg = "Instrument not defined!" 3670 3671 if portfolio is None or not portfolio: 3672 portfolio = self.Overview(show=False) 3673 3674 if self._ticker: 3675 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3676 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3677 3678 for iType in TKS_INSTRUMENTS: 3679 for instrument in portfolio["stat"][iType]: 3680 if instrument["ticker"] == self._ticker: 3681 result = True 3682 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3683 break 3684 3685 elif self._figi: 3686 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3687 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3688 3689 for iType in TKS_INSTRUMENTS: 3690 for instrument in portfolio["stat"][iType]: 3691 if instrument["figi"] == self._figi: 3692 result = True 3693 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3694 break 3695 3696 else: 3697 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3698 3699 uLogger.debug(msg) 3700 3701 return result 3702 3703 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3704 """ 3705 Returns instrument from the user's portfolio if it presents there. 3706 Instrument must be defined by `ticker` (highly priority) or `figi`. 3707 3708 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3709 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3710 """ 3711 result = None 3712 msg = "Instrument not defined!" 3713 3714 if portfolio is None or not portfolio: 3715 portfolio = self.Overview(show=False) 3716 3717 if self._ticker: 3718 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3719 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3720 3721 for iType in TKS_INSTRUMENTS: 3722 for instrument in portfolio["stat"][iType]: 3723 if instrument["ticker"] == self._ticker: 3724 result = instrument 3725 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3726 break 3727 3728 elif self._figi: 3729 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3730 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3731 3732 for iType in TKS_INSTRUMENTS: 3733 for instrument in portfolio["stat"][iType]: 3734 if instrument["figi"] == self._figi: 3735 result = instrument 3736 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3737 break 3738 3739 else: 3740 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3741 3742 uLogger.debug(msg) 3743 3744 return result 3745 3746 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3747 """ 3748 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3749 3750 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3751 3752 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3753 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3754 """ 3755 result = False 3756 msg = "Instrument not defined!" 3757 3758 if portfolio is None or not portfolio: 3759 portfolio = self.Overview(show=False) 3760 3761 if self._ticker: 3762 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3763 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3764 3765 for instrument in portfolio["stat"]["orders"]: 3766 if instrument["ticker"] == self._ticker: 3767 result = True 3768 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3769 break 3770 3771 elif self._figi: 3772 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3773 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3774 3775 for instrument in portfolio["stat"]["orders"]: 3776 if instrument["figi"] == self._figi: 3777 result = True 3778 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3779 break 3780 3781 else: 3782 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3783 3784 uLogger.debug(msg) 3785 3786 return result 3787 3788 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3789 """ 3790 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3791 Instrument must be defined by `ticker` (highly priority) or `figi`. 3792 3793 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3794 3795 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3796 :return: list with `orderID`s of limit orders. 3797 """ 3798 result = [] 3799 msg = "Instrument not defined!" 3800 3801 if portfolio is None or not portfolio: 3802 portfolio = self.Overview(show=False) 3803 3804 if self._ticker: 3805 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3806 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3807 3808 for instrument in portfolio["stat"]["orders"]: 3809 if instrument["ticker"] == self._ticker: 3810 result.append(instrument["orderID"]) 3811 3812 if result: 3813 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3814 3815 elif self._figi: 3816 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3817 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3818 3819 for instrument in portfolio["stat"]["orders"]: 3820 if instrument["figi"] == self._figi: 3821 result.append(instrument["orderID"]) 3822 3823 if result: 3824 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3825 3826 else: 3827 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3828 3829 uLogger.debug(msg) 3830 3831 return result 3832 3833 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3834 """ 3835 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3836 3837 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3838 3839 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3840 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3841 """ 3842 result = False 3843 msg = "Instrument not defined!" 3844 3845 if portfolio is None or not portfolio: 3846 portfolio = self.Overview(show=False) 3847 3848 if self._ticker: 3849 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3850 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3851 3852 for instrument in portfolio["stat"]["stopOrders"]: 3853 if instrument["ticker"] == self._ticker: 3854 result = True 3855 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3856 break 3857 3858 elif self._figi: 3859 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3860 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3861 3862 for instrument in portfolio["stat"]["stopOrders"]: 3863 if instrument["figi"] == self._figi: 3864 result = True 3865 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3866 break 3867 3868 else: 3869 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3870 3871 uLogger.debug(msg) 3872 3873 return result 3874 3875 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3876 """ 3877 Returns list with all `orderID`s of opened stop orders for the instrument. 3878 Instrument must be defined by `ticker` (highly priority) or `figi`. 3879 3880 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3881 3882 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3883 :return: list with `orderID`s of stop orders. 3884 """ 3885 result = [] 3886 msg = "Instrument not defined!" 3887 3888 if portfolio is None or not portfolio: 3889 portfolio = self.Overview(show=False) 3890 3891 if self._ticker: 3892 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3893 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3894 3895 for instrument in portfolio["stat"]["stopOrders"]: 3896 if instrument["ticker"] == self._ticker: 3897 result.append(instrument["orderID"]) 3898 3899 if result: 3900 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3901 3902 elif self._figi: 3903 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3904 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3905 3906 for instrument in portfolio["stat"]["stopOrders"]: 3907 if instrument["figi"] == self._figi: 3908 result.append(instrument["orderID"]) 3909 3910 if result: 3911 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3912 3913 else: 3914 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3915 3916 uLogger.debug(msg) 3917 3918 return result 3919 3920 def RequestLimits(self) -> dict: 3921 """ 3922 Method for obtaining the available funds for withdrawal for current `accountId`. 3923 3924 See also: 3925 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3926 - `OverviewLimits()` method 3927 3928 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3929 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3930 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3931 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3932 """ 3933 if self.accountId is None or not self.accountId: 3934 uLogger.error("Variable `accountId` must be defined for using this method!") 3935 raise Exception("Account ID required") 3936 3937 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3938 3939 self.body = str({"accountId": self.accountId}) 3940 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3941 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3942 3943 if self.moreDebug: 3944 uLogger.debug("Records about available funds for withdrawal successfully received") 3945 3946 return rawLimits 3947 3948 def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict: 3949 """ 3950 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3951 3952 See also: `RequestLimits()`. 3953 3954 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3955 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 3956 :return: dict with raw parsed data from server and some calculated statistics about it. 3957 """ 3958 if self.accountId is None or not self.accountId: 3959 uLogger.error("Variable `accountId` must be defined for using this method!") 3960 raise Exception("Account ID required") 3961 3962 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3963 3964 view = { 3965 "rawLimits": rawLimits, 3966 "limits": { # parsed data for every currency: 3967 "money": { # this is an array of portfolio currency positions 3968 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3969 }, 3970 "blocked": { # this is an array of blocked currency 3971 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3972 }, 3973 "blockedGuarantee": { # this is locked money under collateral for futures 3974 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3975 }, 3976 }, 3977 } 3978 3979 # --- Prepare text table with limits in human-readable format: 3980 if show or onlyFiles: 3981 info = [ 3982 "# Withdrawal limits\n\n", 3983 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3984 "* **Account ID:** [{}]\n".format(self.accountId), 3985 ] 3986 3987 if view["limits"]["money"]: 3988 info.extend([ 3989 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3990 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3991 ]) 3992 3993 else: 3994 info.append("\nNo withdrawal limits\n") 3995 3996 for curr in view["limits"]["money"].keys(): 3997 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3998 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3999 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 4000 4001 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 4002 "[{}]".format(curr), 4003 "{:.2f}".format(view["limits"]["money"][curr]), 4004 "{:.2f}".format(availableMoney), 4005 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 4006 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 4007 ) 4008 4009 if curr == "rub": 4010 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 4011 4012 else: 4013 info.append(infoStr) 4014 4015 infoText = "".join(info) 4016 4017 if show and not onlyFiles: 4018 uLogger.info(infoText) 4019 4020 if self.withdrawalLimitsFile and (show or onlyFiles): 4021 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 4022 fH.write(infoText) 4023 4024 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 4025 4026 if self.useHTMLReports: 4027 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 4028 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4029 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 4030 4031 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4032 4033 return view 4034 4035 def RequestAccounts(self) -> dict: 4036 """ 4037 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 4038 4039 See also: 4040 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 4041 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4042 - `OverviewUserInfo()` method 4043 4044 :return: dict with raw data from server that contains accounts info. Example of dict: 4045 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4046 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4047 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4048 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4049 """ 4050 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4051 4052 self.body = str({}) 4053 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4054 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4055 4056 if self.moreDebug: 4057 uLogger.debug("Records about available accounts successfully received") 4058 4059 return rawAccounts 4060 4061 def RequestUserInfo(self) -> dict: 4062 """ 4063 Method for requesting common user's information. 4064 4065 See also: 4066 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4067 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4068 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4069 - `OverviewUserInfo()` method 4070 4071 :return: dict with raw data from server that contains user's information. Example of dict: 4072 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4073 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4074 """ 4075 uLogger.debug("Requesting common user's information. Wait, please...") 4076 4077 self.body = str({}) 4078 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4079 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4080 4081 if self.moreDebug: 4082 uLogger.debug("Records about current user successfully received") 4083 4084 return rawUserInfo 4085 4086 def RequestMarginStatus(self, accountId: str = None) -> dict: 4087 """ 4088 Method for requesting margin calculation for defined account ID. 4089 4090 See also: 4091 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4092 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4093 - `OverviewUserInfo()` method 4094 4095 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4096 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4097 Example of responses: 4098 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4099 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4100 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4101 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4102 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4103 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4104 """ 4105 if accountId is None or not accountId: 4106 if self.accountId is None or not self.accountId: 4107 uLogger.error("Variable `accountId` must be defined for using this method!") 4108 raise Exception("Account ID required") 4109 4110 else: 4111 accountId = self.accountId # use `self.accountId` (main ID) by default 4112 4113 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4114 4115 self.body = str({"accountId": accountId}) 4116 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4117 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4118 4119 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4120 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4121 rawMargin = {} 4122 4123 else: 4124 if self.moreDebug: 4125 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4126 4127 return rawMargin 4128 4129 def RequestTariffLimits(self) -> dict: 4130 """ 4131 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4132 4133 See also: 4134 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4135 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4136 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4137 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4138 - `OverviewUserInfo()` method 4139 4140 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4141 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4142 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4143 """ 4144 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4145 4146 self.body = str({}) 4147 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4148 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4149 4150 if self.moreDebug: 4151 uLogger.debug("Records with limits of current tariff successfully received") 4152 4153 return rawTariffLimits 4154 4155 def RequestBondCoupons(self, iJSON: dict) -> dict: 4156 """ 4157 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4158 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4159 All dates are in UTC timezone. 4160 4161 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4162 Documentation: 4163 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4164 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4165 4166 See also: `ExtendBondsData()`. 4167 4168 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4169 If raw iJSON is not data of bond then server returns an error [400] with message: 4170 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4171 :return: dictionary with bond payment calendar. Response example 4172 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4173 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4174 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4175 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4176 """ 4177 if iJSON["figi"] is None or not iJSON["figi"]: 4178 uLogger.error("FIGI must be defined for using this method!") 4179 raise Exception("FIGI required") 4180 4181 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4182 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4183 4184 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4185 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4186 self._figi, 4187 startDate, 4188 endDate, 4189 )) 4190 4191 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4192 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4193 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4194 4195 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4196 uLogger.warning("Instrument type is not bond!") 4197 4198 else: 4199 if self.moreDebug: 4200 uLogger.debug("Records about bond payment calendar successfully received") 4201 4202 return calendar 4203 4204 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4205 """ 4206 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4207 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4208 coupon yields, current yields and some statistics etc. 4209 4210 WARNING! This is too long operation if a lot of bonds requested from broker server. 4211 4212 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4213 4214 :param instruments: list of strings with tickers or FIGIs. 4215 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4216 for further used by data scientists or stock analytics. 4217 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4218 In XLSX-file and Pandas DataFrame fields mean: 4219 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4220 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4221 """ 4222 if instruments is None or not instruments: 4223 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4224 raise Exception("Ticker or FIGI required") 4225 4226 if isinstance(instruments, str): 4227 instruments = [instruments] 4228 4229 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4230 4231 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4232 4233 iCount = len(uniqueInstruments) 4234 tooLong = iCount >= 20 4235 if tooLong: 4236 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4237 4238 bonds = None 4239 for i, self._figi in enumerate(uniqueInstruments): 4240 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4241 4242 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4243 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4244 rawBond = self.SearchByFIGI(requestPrice=True) 4245 4246 # Widen raw data with UTC current time (iData["actualDateTime"]): 4247 actualDate = datetime.now(tzutc()) 4248 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4249 4250 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4251 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4252 4253 # Replace some values with human-readable: 4254 iData["nominalCurrency"] = iData["nominal"]["currency"] 4255 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4256 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4257 iData["aciCurrency"] = iData["aciValue"]["currency"] 4258 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4259 iData["issueSize"] = int(iData["issueSize"]) 4260 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4261 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4262 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4263 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4264 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4265 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4266 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4267 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4268 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4269 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4270 4271 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4272 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4273 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4274 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4275 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4276 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4277 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4278 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4279 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4280 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4281 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4282 4283 # Widen raw data with calendar data from `rawCalendar` values: 4284 calendarData = [] 4285 if "events" in iData["rawCalendar"].keys(): 4286 for item in iData["rawCalendar"]["events"]: 4287 calendarData.append({ 4288 "couponDate": item["couponDate"], 4289 "couponNumber": int(item["couponNumber"]), 4290 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4291 "payCurrency": item["payOneBond"]["currency"], 4292 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4293 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4294 "couponStartDate": item["couponStartDate"], 4295 "couponEndDate": item["couponEndDate"], 4296 "couponPeriod": item["couponPeriod"], 4297 }) 4298 4299 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4300 if "maturityDate" not in iData.keys(): 4301 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4302 4303 # Widen raw data with Coupon Rate. 4304 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4305 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4306 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4307 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4308 4309 # Widen raw data with Yield to Maturity (YTM) on current date. 4310 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4311 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4312 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4313 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4314 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4315 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4316 4317 iData["calendar"] = calendarData # adds calendar at the end 4318 4319 # Remove not used data: 4320 iData.pop("uid") 4321 iData.pop("positionUid") 4322 iData.pop("currentPrice") 4323 iData.pop("rawCalendar") 4324 4325 colNames = list(iData.keys()) 4326 if bonds is None: 4327 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4328 4329 else: 4330 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4331 4332 else: 4333 uLogger.warning("Instrument is not a bond!") 4334 4335 processed = round(100 * (i + 1) / iCount, 1) 4336 if tooLong and processed % 5 == 0: 4337 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4338 4339 else: 4340 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4341 4342 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4343 4344 # Saving bonds from Pandas DataFrame to XLSX sheet: 4345 if xlsx and self.bondsXLSXFile: 4346 with pd.ExcelWriter( 4347 path=self.bondsXLSXFile, 4348 date_format=TKS_DATE_FORMAT, 4349 datetime_format=TKS_DATE_TIME_FORMAT, 4350 mode="w", 4351 ) as writer: 4352 bonds.to_excel( 4353 writer, 4354 sheet_name="Extended bonds data", 4355 index=True, 4356 encoding="UTF-8", 4357 freeze_panes=(1, 1), 4358 ) # saving as XLSX-file with freeze first row and column as headers 4359 4360 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4361 4362 return bonds 4363 4364 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4365 """ 4366 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4367 4368 WARNING! This is too long operation if a lot of bonds requested from broker server. 4369 4370 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4371 4372 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4373 extended information about bonds: main info, current prices, bond payment calendar, 4374 coupon yields, current yields and some statistics etc. 4375 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4376 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4377 for further used by data scientists or stock analytics. 4378 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4379 """ 4380 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4381 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4382 4383 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4384 4385 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4386 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4387 calendar = None 4388 for bond in extBonds.iterrows(): 4389 for item in bond[1]["calendar"]: 4390 cData = { 4391 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4392 "couponDate": item["couponDate"], 4393 "figi": bond[1]["figi"], 4394 "ticker": bond[1]["ticker"], 4395 "name": bond[1]["name"], 4396 "couponNumber": item["couponNumber"], 4397 "payOneBond": item["payOneBond"], 4398 "payCurrency": item["payCurrency"], 4399 "couponType": item["couponType"], 4400 "couponPeriod": item["couponPeriod"], 4401 "fixDate": item["fixDate"], 4402 "couponStartDate": item["couponStartDate"], 4403 "couponEndDate": item["couponEndDate"], 4404 } 4405 4406 if calendar is None: 4407 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4408 4409 else: 4410 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4411 4412 if calendar is not None: 4413 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4414 4415 # Saving calendar from Pandas DataFrame to XLSX sheet: 4416 if xlsx: 4417 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4418 4419 with pd.ExcelWriter( 4420 path=xlsxCalendarFile, 4421 date_format=TKS_DATE_FORMAT, 4422 datetime_format=TKS_DATE_TIME_FORMAT, 4423 mode="w", 4424 ) as writer: 4425 humanReadable = calendar.copy(deep=True) 4426 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4427 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4428 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4429 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4430 humanReadable.columns = colNames # human-readable column names 4431 4432 humanReadable.to_excel( 4433 writer, 4434 sheet_name="Bond payments calendar", 4435 index=False, 4436 encoding="UTF-8", 4437 freeze_panes=(1, 2), 4438 ) # saving as XLSX-file with freeze first row and column as headers 4439 4440 del humanReadable # release df in memory 4441 4442 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4443 4444 return calendar 4445 4446 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str: 4447 """ 4448 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4449 Also, creates Markdown file with calendar data, `calendar.md` by default. 4450 4451 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4452 4453 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4454 extended information about bonds: main info, current prices, bond payment calendar, 4455 coupon yields, current yields and some statistics etc. 4456 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4457 :param show: if `True` then also printing bonds payment calendar to the console, 4458 otherwise save to file `calendarFile` only. `False` by default. 4459 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4460 :return: multilines text in Markdown format with bonds payment calendar as a table. 4461 """ 4462 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4463 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles) 4464 4465 infoText = "# Bond payments calendar\n\n" 4466 4467 calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles) # generate Pandas DataFrame with full calendar data 4468 4469 if not (calendar is None or calendar.empty): 4470 splitLine = "| | | | | | | | | |\n" 4471 4472 info = [ 4473 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4474 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4475 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4476 ] 4477 4478 newMonth = False 4479 notOneBond = calendar["figi"].nunique() > 1 4480 for i, bond in enumerate(calendar.iterrows()): 4481 if newMonth and notOneBond: 4482 info.append(splitLine) 4483 4484 info.append( 4485 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4486 " √" if bond[1]["paid"] else " —", 4487 bond[1]["couponDate"].split("T")[0], 4488 bond[1]["figi"], 4489 bond[1]["ticker"], 4490 bond[1]["couponNumber"], 4491 "{} {}".format( 4492 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4493 bond[1]["payCurrency"], 4494 ), 4495 bond[1]["couponType"], 4496 bond[1]["couponPeriod"], 4497 bond[1]["fixDate"].split("T")[0], 4498 ) 4499 ) 4500 4501 if i < len(calendar.values) - 1: 4502 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4503 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4504 newMonth = False if curDate.month == nextDate.month else True 4505 4506 else: 4507 newMonth = False 4508 4509 infoText += "".join(info) 4510 4511 if show and not onlyFiles: 4512 uLogger.info("{}".format(infoText)) 4513 4514 if self.calendarFile is not None and (show or onlyFiles): 4515 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4516 fH.write(infoText) 4517 4518 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4519 4520 if self.useHTMLReports: 4521 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4522 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4523 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4524 4525 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4526 4527 else: 4528 infoText += "No data\n" 4529 4530 return infoText 4531 4532 def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict: 4533 """ 4534 Method for parsing and show simple table with all available user accounts. 4535 4536 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4537 4538 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4539 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4540 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4541 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4542 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4543 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4544 "closed": "—", "access": "Full access" }, ...}}` 4545 """ 4546 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4547 4548 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4549 accounts = { 4550 item["id"]: { 4551 "type": TKS_ACCOUNT_TYPES[item["type"]], 4552 "name": item["name"], 4553 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4554 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4555 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4556 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4557 } for item in rawAccounts["accounts"] 4558 } 4559 4560 # Raw and parsed data with some fields replaced in "stat" section: 4561 view = { 4562 "rawAccounts": rawAccounts, 4563 "stat": accounts, 4564 } 4565 4566 # --- Prepare simple text table with only accounts data in human-readable format: 4567 if show or onlyFiles: 4568 info = [ 4569 "# User accounts\n\n", 4570 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4571 "| Account ID | Type | Status | Name |\n", 4572 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4573 ] 4574 4575 for account in view["stat"].keys(): 4576 info.extend([ 4577 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4578 account, 4579 view["stat"][account]["type"], 4580 view["stat"][account]["status"], 4581 view["stat"][account]["name"], 4582 ) 4583 ]) 4584 4585 infoText = "".join(info) 4586 4587 if show and not onlyFiles: 4588 uLogger.info(infoText) 4589 4590 if self.userAccountsFile and (show or onlyFiles): 4591 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4592 fH.write(infoText) 4593 4594 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4595 4596 if self.useHTMLReports: 4597 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4598 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4599 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4600 4601 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4602 4603 return view 4604 4605 def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict: 4606 """ 4607 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4608 4609 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4610 4611 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4612 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4613 :return: dict with raw parsed data from server and some calculated statistics about it. 4614 """ 4615 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4616 tmpTicker = self._ticker 4617 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4618 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4619 self._ticker = tmpTicker 4620 4621 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4622 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4623 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4624 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4625 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4626 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4627 4628 # This is dict with parsed common user data: 4629 userInfo = { 4630 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4631 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4632 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4633 "tariff": rawUserInfo["tariff"], 4634 } 4635 4636 # This is an array of dict with parsed margin statuses for every account IDs: 4637 margins = {} 4638 for accountId in accounts.keys(): 4639 if rawMargins[accountId]: 4640 margins[accountId] = { 4641 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4642 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4643 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4644 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4645 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4646 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4647 "missing": missing["volume"], 4648 } 4649 4650 else: 4651 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4652 4653 unary = {} # unary-connection limits 4654 for item in rawTariffLimits["unaryLimits"]: 4655 if item["limitPerMinute"] in unary.keys(): 4656 unary[item["limitPerMinute"]].extend(item["methods"]) 4657 4658 else: 4659 unary[item["limitPerMinute"]] = item["methods"] 4660 4661 stream = {} # stream-connection limits 4662 for item in rawTariffLimits["streamLimits"]: 4663 if item["limit"] in stream.keys(): 4664 stream[item["limit"]].extend(item["streams"]) 4665 4666 else: 4667 stream[item["limit"]] = item["streams"] 4668 4669 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4670 limits = { 4671 "unary": unary, 4672 "stream": stream, 4673 } 4674 4675 # Raw and parsed data as an output result: 4676 view = { 4677 "rawUserInfo": rawUserInfo, 4678 "rawAccounts": rawAccounts, 4679 "rawMargins": rawMargins, 4680 "rawTariffLimits": rawTariffLimits, 4681 "stat": { 4682 "overview": overview, 4683 "userInfo": userInfo, 4684 "accounts": accounts, 4685 "margins": margins, 4686 "limits": limits, 4687 }, 4688 } 4689 4690 # --- Prepare text table with user information in human-readable format: 4691 if show or onlyFiles: 4692 info = [ 4693 "# Full user information\n\n", 4694 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4695 "## Common information\n\n", 4696 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4697 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4698 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4699 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4700 "\n## User accounts\n\n", 4701 ] 4702 4703 for account in view["stat"]["accounts"].keys(): 4704 info.extend([ 4705 "### ID: [{}]\n\n".format(account), 4706 "| Parameters | Values |\n", 4707 "|----------------------|--------------------------------------------------------------|\n", 4708 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4709 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4710 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4711 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4712 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4713 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4714 ]) 4715 4716 if margins[account]: 4717 info.extend([ 4718 "| Margin status: | Enabled |\n", 4719 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4720 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4721 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4722 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4723 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4724 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4725 ]) 4726 4727 else: 4728 info.append("| Margin status: | Disabled |\n\n") 4729 4730 info.extend([ 4731 "\n## Current user tariff limits\n", 4732 "\n### See also\n", 4733 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4734 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4735 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4736 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4737 "\n### Unary limits\n", 4738 ]) 4739 4740 if unary: 4741 for key, values in sorted(unary.items()): 4742 info.append("\n* Max requests per minute: {}\n".format(key)) 4743 4744 for value in values: 4745 info.append(" - {}\n".format(value)) 4746 4747 else: 4748 info.append("\nNot available\n") 4749 4750 info.append("\n### Stream limits\n") 4751 4752 if stream: 4753 for key, values in sorted(stream.items()): 4754 info.append("\n* Max stream connections: {}\n".format(key)) 4755 4756 for value in values: 4757 info.append(" - {}\n".format(value)) 4758 4759 else: 4760 info.append("\nNot available\n") 4761 4762 infoText = "".join(info) 4763 4764 if show and not onlyFiles: 4765 uLogger.info(infoText) 4766 4767 if self.userInfoFile and (show or onlyFiles): 4768 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4769 fH.write(infoText) 4770 4771 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4772 4773 if self.useHTMLReports: 4774 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4775 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4776 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4777 4778 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4779 4780 return view 4781 4782 4783class Args: 4784 """ 4785 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4786 """ 4787 def __init__(self, **kwargs): 4788 self.__dict__.update(kwargs) 4789 4790 def __getattr__(self, item): 4791 return None 4792 4793 4794def ParseArgs(): 4795 """This function get and parse command line keys.""" 4796 parser = ArgumentParser() # command-line string parser 4797 4798 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4799 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4800 4801 # --- options: 4802 4803 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4804 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4805 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4806 4807 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4808 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4809 4810 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4811 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4812 4813 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4814 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4815 4816 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4817 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4818 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4819 4820 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4821 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4822 parser.add_argument("--tag", type=str, default="", help="Option: identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).") 4823 4824 # --- commands: 4825 4826 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4827 4828 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4829 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4830 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4831 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4832 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4833 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4834 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4835 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4836 4837 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4838 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4839 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4840 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4841 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4842 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4843 4844 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4845 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4846 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4847 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4848 4849 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4850 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4851 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4852 4853 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4854 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4855 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4856 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4857 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4858 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4859 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4860 4861 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4862 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4863 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4864 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4865 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4866 4867 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4868 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4869 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4870 4871 cmdArgs = parser.parse_args() 4872 return cmdArgs 4873 4874 4875def Main(**kwargs): 4876 """ 4877 Main function for work with TKSBrokerAPI in the console. 4878 4879 See examples: 4880 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4881 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4882 """ 4883 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4884 4885 if args.debug_level: 4886 uLogger.level = 10 # always debug level by default 4887 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4888 4889 exitCode = 0 4890 start = datetime.now(tzutc()) 4891 uLogger.debug("=-" * 50) 4892 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4893 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4894 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4895 )) 4896 4897 # trying to calculate full current version: 4898 buildVersion = __version__ 4899 try: 4900 v = version("tksbrokerapi") 4901 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4902 4903 except Exception: 4904 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4905 4906 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4907 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4908 4909 try: 4910 if args.version: 4911 print("TKSBrokerAPI {}".format(buildVersion)) 4912 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4913 4914 else: 4915 # Init class for trading with Tinkoff Broker: 4916 trader = TinkoffBrokerServer( 4917 token=args.token, 4918 accountId=args.account_id, 4919 useCache=not args.no_cache, 4920 ) 4921 4922 if args.tag is not None: 4923 trader.tag = args.tag # Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode 4924 4925 # --- set some options: 4926 4927 if args.more: 4928 trader.moreDebug = True 4929 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4930 4931 if args.html: 4932 trader.useHTMLReports = True 4933 4934 if args.ticker: 4935 ticker = str(args.ticker).upper() # Tickers may be upper case only 4936 4937 if ticker in trader.aliasesKeys: 4938 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4939 4940 else: 4941 trader.ticker = ticker 4942 4943 if args.figi: 4944 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4945 4946 if args.depth is not None: 4947 trader.depth = args.depth 4948 4949 # --- do one command: 4950 4951 if args.list: 4952 if args.output is not None: 4953 trader.instrumentsFile = args.output 4954 4955 trader.ShowInstrumentsInfo(show=True) 4956 4957 elif args.list_xlsx: 4958 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4959 4960 elif args.bonds_xlsx is not None: 4961 if args.output is not None: 4962 trader.bondsXLSXFile = args.output 4963 4964 if len(args.bonds_xlsx) == 0: 4965 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4966 4967 else: 4968 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4969 4970 elif args.search: 4971 if args.output is not None: 4972 trader.searchResultsFile = args.output 4973 4974 trader.SearchInstruments(pattern=args.search[0], show=True) 4975 4976 elif args.info: 4977 if not (args.ticker or args.figi): 4978 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4979 raise Exception("Ticker or FIGI required") 4980 4981 if args.output is not None: 4982 trader.infoFile = args.output 4983 4984 if args.ticker: 4985 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4986 4987 else: 4988 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4989 4990 elif args.calendar is not None: 4991 if args.output is not None: 4992 trader.calendarFile = args.output 4993 4994 if len(args.calendar) == 0: 4995 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4996 4997 else: 4998 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4999 5000 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 5001 5002 elif args.price: 5003 if not (args.ticker or args.figi): 5004 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5005 raise Exception("Ticker or FIGI required") 5006 5007 trader.GetCurrentPrices(show=True) 5008 5009 elif args.prices is not None: 5010 if args.output is not None: 5011 trader.pricesFile = args.output 5012 5013 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 5014 5015 elif args.overview: 5016 if args.output is not None: 5017 trader.overviewFile = args.output 5018 5019 trader.Overview(show=True, details="full") 5020 5021 elif args.overview_digest: 5022 if args.output is not None: 5023 trader.overviewDigestFile = args.output 5024 5025 trader.Overview(show=True, details="digest") 5026 5027 elif args.overview_positions: 5028 if args.output is not None: 5029 trader.overviewPositionsFile = args.output 5030 5031 trader.Overview(show=True, details="positions") 5032 5033 elif args.overview_orders: 5034 if args.output is not None: 5035 trader.overviewOrdersFile = args.output 5036 5037 trader.Overview(show=True, details="orders") 5038 5039 elif args.overview_analytics: 5040 if args.output is not None: 5041 trader.overviewAnalyticsFile = args.output 5042 5043 trader.Overview(show=True, details="analytics") 5044 5045 elif args.overview_calendar: 5046 if args.output is not None: 5047 trader.overviewAnalyticsFile = args.output 5048 5049 trader.Overview(show=True, details="calendar") 5050 5051 elif args.deals is not None: 5052 if args.output is not None: 5053 trader.reportFile = args.output 5054 5055 if 0 <= len(args.deals) < 3: 5056 trader.Deals( 5057 start=args.deals[0] if len(args.deals) >= 1 else None, 5058 end=args.deals[1] if len(args.deals) == 2 else None, 5059 show=True, # Always show deals report in console 5060 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 5061 ) 5062 5063 else: 5064 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5065 raise Exception("Incorrect value") 5066 5067 elif args.history is not None: 5068 if args.output is not None: 5069 trader.historyFile = args.output 5070 5071 if 0 <= len(args.history) < 3: 5072 dataReceived = trader.History( 5073 start=args.history[0] if len(args.history) >= 1 else None, 5074 end=args.history[1] if len(args.history) == 2 else None, 5075 interval="hour" if args.interval is None or not args.interval else args.interval, 5076 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5077 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5078 show=True, # shows all downloaded candles in console 5079 ) 5080 5081 if args.render_chart is not None and dataReceived is not None: 5082 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5083 5084 trader.ShowHistoryChart( 5085 candles=dataReceived, 5086 interact=iChart, 5087 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5088 ) 5089 5090 else: 5091 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5092 raise Exception("Incorrect value") 5093 5094 elif args.load_history is not None: 5095 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5096 5097 if args.render_chart is not None and histData is not None: 5098 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5099 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5100 5101 trader.ShowHistoryChart( 5102 candles=histData, 5103 interact=iChart, 5104 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5105 ) 5106 5107 elif args.trade is not None: 5108 if 1 <= len(args.trade) <= 5: 5109 trader.Trade( 5110 operation=args.trade[0], 5111 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5112 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5113 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5114 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5115 ) 5116 5117 else: 5118 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5119 5120 elif args.buy is not None: 5121 if 0 <= len(args.buy) <= 4: 5122 trader.Buy( 5123 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5124 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5125 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5126 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5127 ) 5128 5129 else: 5130 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5131 5132 elif args.sell is not None: 5133 if 0 <= len(args.sell) <= 4: 5134 trader.Sell( 5135 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5136 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5137 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5138 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5139 ) 5140 5141 else: 5142 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5143 5144 elif args.order: 5145 if 4 <= len(args.order) <= 7: 5146 trader.Order( 5147 operation=args.order[0], 5148 orderType=args.order[1], 5149 lots=int(args.order[2]), 5150 targetPrice=float(args.order[3]), 5151 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5152 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5153 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5154 ) 5155 5156 else: 5157 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5158 5159 elif args.buy_limit: 5160 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5161 5162 elif args.sell_limit: 5163 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5164 5165 elif args.buy_stop: 5166 if 2 <= len(args.buy_stop) <= 7: 5167 trader.BuyStop( 5168 lots=int(args.buy_stop[0]), 5169 targetPrice=float(args.buy_stop[1]), 5170 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5171 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5172 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5173 ) 5174 5175 else: 5176 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5177 5178 elif args.sell_stop: 5179 if 2 <= len(args.sell_stop) <= 7: 5180 trader.SellStop( 5181 lots=int(args.sell_stop[0]), 5182 targetPrice=float(args.sell_stop[1]), 5183 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5184 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5185 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5186 ) 5187 5188 else: 5189 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5190 5191 # elif args.buy_order_grid is not None: 5192 # # update order grid work with api v2 5193 # if len(args.buy_order_grid) == 2: 5194 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5195 # 5196 # for order in orderParams: 5197 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5198 # 5199 # else: 5200 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5201 # 5202 # elif args.sell_order_grid is not None: 5203 # # update order grid work with api v2 5204 # if len(args.sell_order_grid) >= 2: 5205 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5206 # 5207 # for order in orderParams: 5208 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5209 # 5210 # else: 5211 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5212 5213 elif args.close_order is not None: 5214 trader.CloseOrders(args.close_order) # close only one order 5215 5216 elif args.close_orders is not None: 5217 trader.CloseOrders(args.close_orders) # close list of orders 5218 5219 elif args.close_trade: 5220 if not (args.ticker or args.figi): 5221 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5222 raise Exception("Ticker or FIGI required") 5223 5224 if args.ticker: 5225 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5226 5227 else: 5228 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5229 5230 elif args.close_trades is not None: 5231 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5232 5233 elif args.close_all is not None: 5234 if args.ticker: 5235 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5236 5237 elif args.figi: 5238 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5239 5240 else: 5241 trader.CloseAll(*args.close_all) 5242 5243 elif args.limits: 5244 if args.output is not None: 5245 trader.withdrawalLimitsFile = args.output 5246 5247 trader.OverviewLimits(show=True) 5248 5249 elif args.user_info: 5250 if args.output is not None: 5251 trader.userInfoFile = args.output 5252 5253 trader.OverviewUserInfo(show=True) 5254 5255 elif args.account: 5256 if args.output is not None: 5257 trader.userAccountsFile = args.output 5258 5259 trader.OverviewAccounts(show=True) 5260 5261 else: 5262 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5263 raise Exception("There is no command to execute") 5264 5265 except Exception: 5266 trace = tb.format_exc() 5267 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5268 if e in trace: 5269 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5270 break 5271 5272 uLogger.debug(trace) 5273 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5274 exitCode = 255 # an error occurred, must be open a ticket for this issue 5275 5276 finally: 5277 finish = datetime.now(tzutc()) 5278 5279 if exitCode == 0: 5280 if args.more: 5281 uLogger.debug("All operations were finished success (summary code is 0).") 5282 5283 else: 5284 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5285 os.path.abspath(uLog.defaultLogFile), exitCode, 5286 )) 5287 5288 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5289 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5290 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5291 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5292 )) 5293 uLogger.debug("=-" * 50) 5294 5295 if not kwargs: 5296 sys.exit(exitCode) 5297 5298 else: 5299 return exitCode 5300 5301 5302if __name__ == "__main__": 5303 Main()
80class TinkoffBrokerServer: 81 """ 82 This class implements methods to work with Tinkoff broker server. 83 84 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 85 86 About `token`: https://tinkoff.github.io/investAPI/token/ 87 """ 88 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 89 """ 90 Main class init. 91 92 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 93 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 94 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 95 :param useCache: use default cache file with raw data to use instead of `iList`. 96 True by default. Cache is auto-update if new day has come. 97 If you don't want to use cache and always updates raw data then set `useCache=False`. 98 :param defaultCache: path to default cache file. `dump.json` by default. 99 """ 100 if token is None or not token: 101 try: 102 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 103 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 104 105 except KeyError: 106 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 107 raise Exception("Token required") 108 109 else: 110 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 111 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 112 113 if accountId is None or not accountId: 114 try: 115 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 116 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 117 118 except KeyError: 119 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 120 121 else: 122 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 123 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 124 125 self.version = __version__ # duplicate here used TKSBrokerAPI main version 126 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 127 128 Latest version: https://pypi.org/project/tksbrokerapi/ 129 """ 130 131 self._tag = "" 132 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 133 134 self.__lock = Lock() # initialize multiprocessing mutex lock 135 136 self._precision = 4 # precision, signs after comma, e.g. 2 for instruments like PLZL, 4 for instruments like USDRUB, if -1 then auto detect it when load data-file 137 138 self.aliases = TKS_TICKER_ALIASES 139 """Some aliases instead official tickers. 140 141 See also: `TKSEnums.TKS_TICKER_ALIASES` 142 """ 143 144 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 145 146 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 147 148 self._ticker = "" 149 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 150 151 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 152 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 153 154 See also: `SearchByTicker()`, `SearchInstruments()`. 155 """ 156 157 self._figi = "" 158 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 159 160 See also: `SearchByFIGI()`, `SearchInstruments()`. 161 """ 162 163 self.depth = 1 164 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 165 166 See also: `GetCurrentPrices()`. 167 """ 168 169 self.server = r"https://invest-public-api.tinkoff.ru/rest" 170 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 171 172 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 173 """ 174 175 uLogger.debug("Broker API server: {}".format(self.server)) 176 177 self.timeout = 15 178 """Server operations timeout in seconds. Default: `15`. 179 180 See also: `SendAPIRequest()`. 181 """ 182 183 self.headers = { 184 "Content-Type": "application/json", 185 "accept": "application/json", 186 "Authorization": "Bearer {}".format(self.token), 187 "x-app-name": "Tim55667757.TKSBrokerAPI", 188 } 189 """ 190 Headers which send in every request to broker server. Please, do not change it! 191 Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`. 192 193 See also: `SendAPIRequest()`. 194 """ 195 196 self.body = None 197 """Request body which send to broker server. Default: `None`. 198 199 See also: `SendAPIRequest()`. 200 """ 201 202 self.moreDebug = False 203 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 204 205 self.useHTMLReports = False 206 """ 207 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 208 209 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 210 """ 211 212 self.historyFile = None 213 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 214 215 See also: `History()`. 216 """ 217 218 self.htmlHistoryFile = "index.html" 219 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 220 221 See also: `ShowHistoryChart()`. 222 """ 223 224 self.instrumentsFile = "instruments.md" 225 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 226 227 See also: `ShowInstrumentsInfo()`. 228 """ 229 230 self.searchResultsFile = "search-results.md" 231 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 232 233 See also: `SearchInstruments()`. 234 """ 235 236 self.pricesFile = "prices.md" 237 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 238 239 See also: `GetListOfPrices()`. 240 """ 241 242 self.infoFile = "info.md" 243 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 244 245 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 246 """ 247 248 self.bondsXLSXFile = "ext-bonds.xlsx" 249 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 250 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 251 252 See also: `ExtendBondsData()`. 253 """ 254 255 self.calendarFile = "calendar.md" 256 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 257 258 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 259 260 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 261 """ 262 263 self.overviewFile = "overview.md" 264 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 265 266 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 267 """ 268 269 self.overviewDigestFile = "overview-digest.md" 270 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 271 272 See also: `Overview()` with parameter `details="digest"`. 273 """ 274 275 self.overviewPositionsFile = "overview-positions.md" 276 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 277 278 See also: `Overview()` with parameter `details="positions"`. 279 """ 280 281 self.overviewOrdersFile = "overview-orders.md" 282 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 283 284 See also: `Overview()` with parameter `details="orders"`. 285 """ 286 287 self.overviewAnalyticsFile = "overview-analytics.md" 288 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 289 290 See also: `Overview()` with parameter `details="analytics"`. 291 """ 292 293 self.overviewBondsCalendarFile = "overview-calendar.md" 294 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 295 296 See also: `Overview()` with parameter `details="calendar"`. 297 """ 298 299 self.reportFile = "deals.md" 300 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 301 302 See also: `Deals()`. 303 """ 304 305 self.withdrawalLimitsFile = "limits.md" 306 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 307 308 See also: `OverviewLimits()` and `RequestLimits()`. 309 """ 310 311 self.userInfoFile = "user-info.md" 312 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 313 314 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 315 """ 316 317 self.userAccountsFile = "accounts.md" 318 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 319 320 See also: `OverviewAccounts()`, `RequestAccounts()`. 321 """ 322 323 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 324 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 325 326 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 327 328 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 329 """ 330 331 self.iList = None # init iList for raw instruments data 332 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 333 334 See also: `Listing()`, `DumpInstruments()`. 335 """ 336 337 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 338 if useCache: 339 if os.path.exists(self.iListDumpFile): 340 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 341 curTime = datetime.now(tzutc()) 342 343 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 344 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 345 346 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 347 348 else: 349 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 350 351 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 352 os.path.abspath(self.iListDumpFile), 353 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 354 )) 355 356 else: 357 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 358 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 359 360 else: 361 self.iList = self.Listing() # request new raw instruments data from broker server 362 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 363 364 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 365 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 366 367 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 368 """ 369 370 @property 371 def tag(self) -> str: 372 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 373 return self._tag 374 375 @tag.setter 376 def tag(self, value): 377 """Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 378 self._tag = str(value) 379 380 if self._tag: 381 for handler in uLogger.handlers: 382 handler.setFormatter(uLog.logging.Formatter(uLog.formatStringWithTag.format(tag=self._tag))) 383 384 uLogger.debug("Custom TKSBrokerAPI tag was set: {}".format(self._tag)) 385 386 else: 387 for handler in uLogger.handlers: 388 handler.setFormatter(uLog.logging.Formatter(uLog.formatString)) 389 390 uLogger.debug("Default logger format is used") 391 392 @property 393 def ticker(self) -> str: 394 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 395 396 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 397 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 398 399 See also: `SearchByTicker()`, `SearchInstruments()`. 400 """ 401 return self._ticker 402 403 @ticker.setter 404 def ticker(self, value): 405 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 406 407 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 408 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 409 410 See also: `SearchByTicker()`, `SearchInstruments()`. 411 """ 412 self._ticker = str(value).upper() # Tickers may be upper case only 413 414 @property 415 def figi(self) -> str: 416 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 417 418 See also: `SearchByFIGI()`, `SearchInstruments()`. 419 """ 420 return self._figi 421 422 @figi.setter 423 def figi(self, value): 424 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 425 426 See also: `SearchByFIGI()`, `SearchInstruments()`. 427 """ 428 self._figi = str(value).upper() # FIGI may be upper case only 429 430 def _ParseJSON(self, rawData="{}") -> dict: 431 """ 432 Parse JSON from response string. 433 434 :param rawData: this is a string with JSON-formatted text. 435 :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`. 436 """ 437 try: 438 responseJSON = json.loads(rawData) if rawData else {} 439 440 if self.moreDebug: 441 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 442 443 return responseJSON 444 445 except Exception as e: 446 uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e)) 447 448 return {} 449 450 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 451 """ 452 Send GET or POST request to broker server and receive JSON object. 453 454 self.header: must be defining with dictionary of headers. 455 self.body: if define then used as request body. None by default. 456 self.timeout: global request timeout, 15 seconds by default. 457 :param url: url with REST request. 458 :param reqType: send "GET" or "POST" request. "GET" by default. 459 :param retry: how many times retry after first request if an 5xx server errors occurred. 460 :param pause: sleep time in seconds between retries. 461 :return: response JSON (dictionary) from broker. 462 """ 463 if reqType.upper() not in ("GET", "POST"): 464 uLogger.error("You can define request type: `GET` or `POST`!") 465 raise Exception("Incorrect value") 466 467 if self.moreDebug: 468 uLogger.debug("Request parameters:") 469 uLogger.debug(" - REST API URL: {}".format(url)) 470 uLogger.debug(" - request type: {}".format(reqType)) 471 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 472 uLogger.debug(" - body:\n{}".format(self.body)) 473 474 # fast hack to avoid all operations with some tickers/FIGI 475 responseJSON = {} 476 oK = True 477 for item in self.exclude: 478 if item in url: 479 if self.moreDebug: 480 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 481 482 oK = False 483 break 484 485 if oK: 486 with self.__lock: # acquire the mutex lock 487 counter = 0 488 response = None 489 errMsg = "" 490 491 while not response and counter <= retry: 492 if reqType == "GET": 493 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 494 495 if reqType == "POST": 496 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 497 498 if self.moreDebug: 499 uLogger.debug("Response:") 500 uLogger.debug(" - status code: {}".format(response.status_code)) 501 uLogger.debug(" - reason: {}".format(response.reason)) 502 uLogger.debug(" - body length: {}".format(len(response.text))) 503 uLogger.debug(" - headers:\n{}".format(response.headers)) 504 505 # Server returns some headers: 506 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 507 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 508 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 509 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 510 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 511 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 512 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 513 sleep(rateLimitWait) 514 515 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 516 if 400 <= response.status_code < 500: 517 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 518 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 519 520 if "code" in response.text and "message" in response.text: 521 msgDict = self._ParseJSON(rawData=response.text) 522 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 523 524 counter = retry + 1 # do not retry for 4xx errors 525 526 if 500 <= response.status_code < 600: 527 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 528 uLogger.debug(" - not oK, {}".format(errMsg)) 529 530 if "code" in response.text and "message" in response.text: 531 errMsgDict = self._ParseJSON(rawData=response.text) 532 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 533 534 counter += 1 535 536 if counter <= retry: 537 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 538 sleep(pause) 539 540 responseJSON = self._ParseJSON(rawData=response.text) 541 542 if errMsg: 543 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 544 uLogger.error(" - not oK, {}".format(errMsg)) 545 546 return responseJSON 547 548 def _IUpdater(self, iType: str) -> tuple: 549 """ 550 Request instrument by type from server. See available API methods for instruments: 551 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 552 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 553 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 554 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 555 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 556 557 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 558 :return: tuple with iType name and list of available instruments of current type for defined user token. 559 """ 560 result = [] 561 562 if iType in TKS_INSTRUMENTS: 563 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 564 565 # all instruments have the same body in API v2 requests: 566 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 567 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 568 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 569 570 return iType, result 571 572 def _IWrapper(self, kwargs): 573 """ 574 Wrapper runs instrument's update method `_IUpdater()`. 575 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 576 """ 577 return self._IUpdater(**kwargs) 578 579 def Listing(self) -> dict: 580 """ 581 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 582 583 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 584 """ 585 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 586 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 587 588 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 589 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 590 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 591 592 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 593 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 594 poolUpdater.close() # close the thread pool 595 poolUpdater.join() # wait a moment until all data returns from threads 596 597 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 598 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 599 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 600 601 # calculate minimum price increment (step) for all instruments and set up instrument's type: 602 for iType in iList.keys(): 603 for ticker in iList[iType]: 604 iList[iType][ticker]["type"] = iType 605 606 if "minPriceIncrement" in iList[iType][ticker].keys(): 607 iList[iType][ticker]["step"] = NanoToFloat( 608 iList[iType][ticker]["minPriceIncrement"]["units"], 609 iList[iType][ticker]["minPriceIncrement"]["nano"], 610 ) 611 612 else: 613 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 614 615 return iList 616 617 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 618 """ 619 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 620 621 See also: `DumpInstruments()`, `Listing()`. 622 623 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 624 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 625 """ 626 if self.iListDumpFile is None or not self.iListDumpFile: 627 uLogger.error("Output name of dump file must be defined!") 628 raise Exception("Filename required") 629 630 if not self.iList or forceUpdate: 631 self.iList = self.Listing() 632 633 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 634 635 # Save as XLSX with separated sheets for every type of instruments: 636 with pd.ExcelWriter( 637 path=xlsxDumpFile, 638 date_format=TKS_DATE_FORMAT, 639 datetime_format=TKS_DATE_TIME_FORMAT, 640 mode="w", 641 ) as writer: 642 for iType in TKS_INSTRUMENTS: 643 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 644 df = df[sorted(df)] # sorted by column names 645 df = df.applymap( 646 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 647 na_action="ignore", 648 ) # converting numbers from nano-type to float in every cell 649 df.to_excel( 650 writer, 651 sheet_name=iType, 652 encoding="UTF-8", 653 freeze_panes=(1, 1), 654 ) # saving as XLSX-file with freeze first row and column as headers 655 656 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 657 658 def DumpInstruments(self, forceUpdate: bool = True) -> str: 659 """ 660 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 661 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 662 663 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 664 665 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 666 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 667 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 668 """ 669 if self.iListDumpFile is None or not self.iListDumpFile: 670 uLogger.error("Output name of dump file must be defined!") 671 raise Exception("Filename required") 672 673 if not self.iList or forceUpdate: 674 self.iList = self.Listing() 675 676 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 677 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 678 fH.write(jsonDump) 679 680 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 681 682 return jsonDump 683 684 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str: 685 """ 686 Show information about one instrument defined by json data and prints it in Markdown format. 687 688 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 689 690 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 691 :param show: if `True` then also printing information about instrument and its current price. 692 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 693 :return: multilines text in Markdown format with information about one instrument. 694 """ 695 splitLine = "| | |\n" 696 infoText = "" 697 698 if iJSON is not None and iJSON and isinstance(iJSON, dict): 699 info = [ 700 "# Main information\n\n", 701 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 702 "| Parameters | Values |\n", 703 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 704 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 705 "| Full name: | {:<54} |\n".format(iJSON["name"]), 706 ] 707 708 if "sector" in iJSON.keys() and iJSON["sector"]: 709 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 710 711 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 712 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 713 714 info.extend([ 715 splitLine, 716 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 717 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 718 ]) 719 720 if "isin" in iJSON.keys() and iJSON["isin"]: 721 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 722 723 if "classCode" in iJSON.keys(): 724 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 725 726 info.extend([ 727 splitLine, 728 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 729 splitLine, 730 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 731 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 732 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 733 ]) 734 735 if iJSON["figi"]: 736 self._figi = iJSON["figi"] 737 iJSON = iJSON | self.RequestTradingStatus() 738 739 info.extend([ 740 splitLine, 741 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 742 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 743 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 744 ]) 745 746 info.append(splitLine) 747 748 if "type" in iJSON.keys() and iJSON["type"]: 749 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 750 751 if "shareType" in iJSON.keys() and iJSON["shareType"]: 752 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 753 754 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 755 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 756 757 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 758 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 759 760 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 761 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 762 763 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 764 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 765 766 if "focusType" in iJSON.keys() and iJSON["focusType"]: 767 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 768 769 if "assetType" in iJSON.keys() and iJSON["assetType"]: 770 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 771 772 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 773 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 774 775 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 776 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 777 778 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 779 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 780 781 if "currency" in iJSON.keys(): 782 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 783 784 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 785 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 786 787 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 788 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 789 790 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 791 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 792 793 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 794 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 795 796 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 797 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 798 799 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 800 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 801 802 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 803 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 804 805 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 806 info.append("| Perpetual bond: | Yes |\n") 807 808 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 809 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 810 811 iExt = None 812 if iJSON["type"] == "Bonds": 813 info.extend([ 814 splitLine, 815 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 816 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 817 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 818 iJSON["nominal"]["currency"], 819 )), 820 ]) 821 822 if "floatingCouponFlag" in iJSON.keys(): 823 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 824 825 if "amortizationFlag" in iJSON.keys(): 826 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 827 828 info.append(splitLine) 829 830 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 831 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 832 833 if iJSON["figi"]: 834 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 835 836 info.extend([ 837 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 838 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 839 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 840 ]) 841 842 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 843 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 844 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 845 iJSON["aciValue"]["currency"] 846 ))) 847 848 if "currentPrice" in iJSON.keys(): 849 info.append(splitLine) 850 851 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 852 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 853 854 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 855 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 856 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 857 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 858 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 859 860 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 861 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 862 863 info.extend([ 864 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 865 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 866 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 867 )), 868 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 869 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 870 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 871 )), 872 "| Changes between last deal price and last close | {:<54} |\n".format( 873 "{:.2f}%{}".format( 874 iJSON["currentPrice"]["changes"], 875 " ({}{:.2f} {})".format( 876 "+" if bondChangesDelta > 0 else "", 877 bondChangesDelta, 878 aciCurrency 879 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 880 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 881 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 882 currency 883 ), 884 ) 885 ), 886 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 887 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 888 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 889 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 890 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 891 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 892 )), 893 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 894 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 895 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 896 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 897 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 898 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 899 )), 900 ]) 901 902 if "lot" in iJSON.keys(): 903 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 904 905 if "step" in iJSON.keys() and iJSON["step"] != 0: 906 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 907 908 # Add bond payment calendar: 909 if iJSON["type"] == "Bonds": 910 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 911 info.extend(["\n#", strCalendar]) 912 913 infoText += "".join(info) 914 915 if show and not onlyFiles: 916 uLogger.info("{}".format(infoText)) 917 918 if self.infoFile is not None and (show or onlyFiles): 919 with open(self.infoFile, "w", encoding="UTF-8") as fH: 920 fH.write(infoText) 921 922 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 923 924 if self.useHTMLReports: 925 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 926 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 927 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 928 929 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 930 931 return infoText 932 933 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 934 """ 935 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 936 937 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 938 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 939 :return: JSON formatted data with information about instrument. 940 """ 941 tickerJSON = {} 942 if self.moreDebug: 943 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 944 945 if not self._ticker: 946 uLogger.warning("self._ticker variable is not be empty!") 947 948 else: 949 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 950 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 951 raise Exception("Instrument not allowed") 952 953 if not self.iList: 954 self.iList = self.Listing() 955 956 if self._ticker in self.iList["Shares"].keys(): 957 tickerJSON = self.iList["Shares"][self._ticker] 958 if self.moreDebug: 959 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 960 961 elif self._ticker in self.iList["Currencies"].keys(): 962 tickerJSON = self.iList["Currencies"][self._ticker] 963 if self.moreDebug: 964 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 965 966 elif self._ticker in self.iList["Bonds"].keys(): 967 tickerJSON = self.iList["Bonds"][self._ticker] 968 if self.moreDebug: 969 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 970 971 elif self._ticker in self.iList["Etfs"].keys(): 972 tickerJSON = self.iList["Etfs"][self._ticker] 973 if self.moreDebug: 974 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 975 976 elif self._ticker in self.iList["Futures"].keys(): 977 tickerJSON = self.iList["Futures"][self._ticker] 978 if self.moreDebug: 979 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 980 981 if tickerJSON: 982 self._figi = tickerJSON["figi"] 983 984 if requestPrice: 985 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 986 987 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 988 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 989 990 else: 991 tickerJSON["currentPrice"]["changes"] = 0 992 993 if show: 994 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 995 996 else: 997 if show: 998 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 999 1000 return tickerJSON 1001 1002 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1003 """ 1004 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1005 1006 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1007 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1008 :return: JSON formatted data with information about instrument. 1009 """ 1010 figiJSON = {} 1011 if self.moreDebug: 1012 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 1013 1014 if not self._figi: 1015 uLogger.warning("self._figi variable is not be empty!") 1016 1017 else: 1018 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1019 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 1020 raise Exception("Instrument not allowed") 1021 1022 if not self.iList: 1023 self.iList = self.Listing() 1024 1025 for item in self.iList["Shares"].keys(): 1026 if self._figi == self.iList["Shares"][item]["figi"]: 1027 figiJSON = self.iList["Shares"][item] 1028 1029 if self.moreDebug: 1030 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1031 1032 break 1033 1034 if not figiJSON: 1035 for item in self.iList["Currencies"].keys(): 1036 if self._figi == self.iList["Currencies"][item]["figi"]: 1037 figiJSON = self.iList["Currencies"][item] 1038 1039 if self.moreDebug: 1040 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1041 1042 break 1043 1044 if not figiJSON: 1045 for item in self.iList["Bonds"].keys(): 1046 if self._figi == self.iList["Bonds"][item]["figi"]: 1047 figiJSON = self.iList["Bonds"][item] 1048 1049 if self.moreDebug: 1050 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1051 1052 break 1053 1054 if not figiJSON: 1055 for item in self.iList["Etfs"].keys(): 1056 if self._figi == self.iList["Etfs"][item]["figi"]: 1057 figiJSON = self.iList["Etfs"][item] 1058 1059 if self.moreDebug: 1060 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1061 1062 break 1063 1064 if not figiJSON: 1065 for item in self.iList["Futures"].keys(): 1066 if self._figi == self.iList["Futures"][item]["figi"]: 1067 figiJSON = self.iList["Futures"][item] 1068 1069 if self.moreDebug: 1070 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1071 1072 break 1073 1074 if figiJSON: 1075 self._figi = figiJSON["figi"] 1076 self._ticker = figiJSON["ticker"] 1077 1078 if requestPrice: 1079 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1080 1081 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1082 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1083 1084 else: 1085 figiJSON["currentPrice"]["changes"] = 0 1086 1087 if show: 1088 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1089 1090 else: 1091 if show: 1092 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1093 1094 return figiJSON 1095 1096 def GetCurrentPrices(self, show: bool = True) -> dict: 1097 """ 1098 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1099 `{"buy": [{"price": 1243.8, "quantity": 193}, 1100 {"price": 1244.0, "quantity": 168}, 1101 {"price": 1244.8, "quantity": 5}, 1102 {"price": 1245.0, "quantity": 61}, 1103 {"price": 1245.4, "quantity": 60}], 1104 "sell": [{"price": 1243.6, "quantity": 8}, 1105 {"price": 1242.6, "quantity": 10}, 1106 {"price": 1242.4, "quantity": 18}, 1107 {"price": 1242.2, "quantity": 50}, 1108 {"price": 1242.0, "quantity": 113}], 1109 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1110 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1111 - sell: list of dicts with Buyers prices, 1112 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1113 - quantity: volume value by current price in lots, 1114 - limitUp: current trade session limit price, maximum, 1115 - limitDown: current trade session limit price, minimum, 1116 - lastPrice: last deal price of the instrument, 1117 - closePrice: previous trade session close price of the instrument. 1118 1119 See also: `SearchByTicker()` and `SearchByFIGI()`. 1120 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1121 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1122 1123 :param show: if `True` then print DOM to log and console. 1124 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1125 If an error occurred then returns an empty record: 1126 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1127 """ 1128 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1129 1130 if self.depth < 1: 1131 uLogger.error("Depth of Market (DOM) must be >=1!") 1132 raise Exception("Incorrect value") 1133 1134 if not (self._ticker or self._figi): 1135 uLogger.error("self._ticker or self._figi variables must be defined!") 1136 raise Exception("Ticker or FIGI required") 1137 1138 if self._ticker and not self._figi: 1139 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1140 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1141 1142 if not self._ticker and self._figi: 1143 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1144 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1145 1146 if not self._figi: 1147 uLogger.error("FIGI is not defined!") 1148 raise Exception("Ticker or FIGI required") 1149 1150 else: 1151 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1152 1153 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1154 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1155 self.body = str({"figi": self._figi, "depth": self.depth}) 1156 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1157 1158 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1159 # list of dicts with sellers orders: 1160 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1161 1162 # list of dicts with buyers orders: 1163 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1164 1165 # max price of instrument at this time: 1166 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1167 1168 # min price of instrument at this time: 1169 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1170 1171 # last price of deal with instrument: 1172 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1173 1174 # last close price of instrument: 1175 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1176 1177 else: 1178 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1179 uLogger.debug("Server response: {}".format(pricesResponse)) 1180 1181 if show: 1182 if prices["buy"] or prices["sell"]: 1183 info = [ 1184 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1185 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1186 self._ticker, 1187 self._figi, 1188 self.depth, 1189 ), 1190 "-" * 60, "\n", 1191 " Orders of Buyers | Orders of Sellers\n", 1192 "-" * 60, "\n", 1193 " Sell prices (volumes) | Buy prices (volumes)\n", 1194 "-" * 60, "\n", 1195 ] 1196 1197 if not prices["buy"]: 1198 info.append(" | No orders!\n") 1199 sumBuy = 0 1200 1201 else: 1202 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1203 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1204 for item in maxMinSorted: 1205 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1206 1207 if not prices["sell"]: 1208 info.append("No orders! |\n") 1209 sumSell = 0 1210 1211 else: 1212 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1213 for item in prices["sell"]: 1214 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1215 1216 info.extend([ 1217 "-" * 60, "\n", 1218 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1219 "-" * 60, "\n", 1220 ]) 1221 1222 infoText = "".join(info) 1223 1224 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1225 1226 else: 1227 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1228 1229 return prices 1230 1231 def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str: 1232 """ 1233 This method get and show information about all available broker instruments for current user account. 1234 If `instrumentsFile` string is not empty then also save information to this file. 1235 1236 :param show: if `True` then print results to console, if `False` — print only to file. 1237 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1238 :return: multi-lines string with all available broker instruments. 1239 """ 1240 if not self.iList: 1241 self.iList = self.Listing() 1242 1243 info = [ 1244 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1245 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1246 ] 1247 1248 # add instruments count by type: 1249 for iType in self.iList.keys(): 1250 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1251 1252 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1253 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1254 1255 # generating info tables with all instruments by type: 1256 for iType in self.iList.keys(): 1257 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1258 1259 for instrument in self.iList[iType].keys(): 1260 iName = self.iList[iType][instrument]["name"] # instrument's name 1261 if len(iName) > 57: 1262 iName = "{}...".format(iName[:54]) # right trim for a long string 1263 1264 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1265 self.iList[iType][instrument]["ticker"], 1266 iName, 1267 self.iList[iType][instrument]["figi"], 1268 self.iList[iType][instrument]["currency"], 1269 self.iList[iType][instrument]["lot"], 1270 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1271 )) 1272 1273 infoText = "".join(info) 1274 1275 if show and not onlyFiles: 1276 uLogger.info(infoText) 1277 1278 if self.instrumentsFile and (show or onlyFiles): 1279 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1280 fH.write(infoText) 1281 1282 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1283 1284 if self.useHTMLReports: 1285 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1286 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1287 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1288 1289 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1290 1291 return infoText 1292 1293 def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict: 1294 """ 1295 This method search and show information about instruments by part of its ticker, FIGI or name. 1296 If `searchResultsFile` string is not empty then also save information to this file. 1297 1298 :param pattern: string with part of ticker, FIGI or instrument's name. 1299 :param show: if `True` then print results to console, if `False` — return list of result only. 1300 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1301 :return: list of dictionaries with all found instruments. 1302 """ 1303 if not self.iList: 1304 self.iList = self.Listing() 1305 1306 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1307 compiledPattern = re.compile(pattern, re.IGNORECASE) 1308 1309 for iType in self.iList: 1310 for instrument in self.iList[iType].values(): 1311 searchResult = compiledPattern.search(" ".join( 1312 [instrument["ticker"], instrument["figi"], instrument["name"]] 1313 )) 1314 1315 if searchResult: 1316 searchResults[iType][instrument["ticker"]] = instrument 1317 1318 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1319 info = [ 1320 "# Search results\n\n", 1321 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1322 "* **Search pattern:** [{}]\n".format(pattern), 1323 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1324 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1325 ] 1326 infoShort = info[:] 1327 1328 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1329 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1330 skippedLine = "| ... | ... | ... | ... |\n" 1331 1332 if resultsLen == 0: 1333 info.append("\nNo results\n") 1334 infoShort.append("\nNo results\n") 1335 uLogger.warning("No results. Try changing your search pattern.") 1336 1337 else: 1338 for iType in searchResults: 1339 iTypeValuesCount = len(searchResults[iType].values()) 1340 if iTypeValuesCount > 0: 1341 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1342 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1343 1344 for instrument in searchResults[iType].values(): 1345 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1346 instrument["type"], 1347 instrument["ticker"], 1348 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1349 instrument["figi"], 1350 )) 1351 1352 if iTypeValuesCount <= 5: 1353 infoShort.extend(info[-iTypeValuesCount:]) 1354 1355 else: 1356 infoShort.extend(info[-5:]) 1357 infoShort.append(skippedLine) 1358 1359 infoText = "".join(info) 1360 infoTextShort = "".join(infoShort) 1361 1362 if show and not onlyFiles: 1363 uLogger.info(infoTextShort) 1364 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1365 1366 if self.searchResultsFile and (show or onlyFiles): 1367 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1368 fH.write(infoText) 1369 1370 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1371 1372 if self.useHTMLReports: 1373 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1374 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1375 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1376 1377 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1378 1379 return searchResults 1380 1381 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1382 """ 1383 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1384 1385 :param instruments: list of strings with tickers or FIGIs. 1386 :return: list with unique instrument FIGIs only. 1387 """ 1388 requestedInstruments = [] 1389 for iName in instruments: 1390 if iName not in self.aliases.keys(): 1391 if iName not in requestedInstruments: 1392 requestedInstruments.append(iName) 1393 1394 else: 1395 if iName not in requestedInstruments: 1396 if self.aliases[iName] not in requestedInstruments: 1397 requestedInstruments.append(self.aliases[iName]) 1398 1399 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1400 1401 onlyUniqueFIGIs = [] 1402 for iName in requestedInstruments: 1403 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1404 continue 1405 1406 self._ticker = iName 1407 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1408 1409 if not iData: 1410 self._ticker = "" 1411 self._figi = iName 1412 1413 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1414 1415 if not iData: 1416 self._figi = "" 1417 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1418 1419 if iData and iData["figi"] not in onlyUniqueFIGIs: 1420 onlyUniqueFIGIs.append(iData["figi"]) 1421 1422 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1423 1424 return onlyUniqueFIGIs 1425 1426 def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]: 1427 """ 1428 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1429 1430 See limits: https://tinkoff.github.io/investAPI/limits/ 1431 1432 If `pricesFile` string is not empty then also save information to this file. 1433 1434 :param instruments: list of strings with tickers or FIGIs. 1435 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1436 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1437 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1438 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1439 """ 1440 if instruments is None or not instruments: 1441 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1442 raise Exception("Ticker or FIGI required") 1443 1444 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1445 1446 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1447 1448 iList = [] # trying to get info and current prices about all unique instruments: 1449 for self._figi in onlyUniqueFIGIs: 1450 iData = self.SearchByFIGI(requestPrice=True, show=False) 1451 iList.append(iData) 1452 1453 self.ShowListOfPrices(iList, show, onlyFiles) 1454 1455 return iList 1456 1457 def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str: 1458 """ 1459 Show table contains current prices of given instruments. 1460 1461 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1462 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1463 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1464 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1465 :return: multilines text in Markdown format as a table contains current prices. 1466 """ 1467 infoText = "" 1468 1469 if show or self.pricesFile or onlyFiles: 1470 info = [ 1471 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1472 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1473 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1474 ] 1475 1476 for item in iList: 1477 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1478 item["ticker"], 1479 item["figi"], 1480 item["type"], 1481 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1482 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1483 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1484 "{} / {}".format( 1485 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1486 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1487 ), 1488 "{} / {}".format( 1489 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1490 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1491 ), 1492 item["currency"], 1493 )) 1494 1495 infoText = "".join(info) 1496 1497 if show and not onlyFiles: 1498 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1499 1500 if self.pricesFile and (show or onlyFiles): 1501 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1502 fH.write(infoText) 1503 1504 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1505 1506 if self.useHTMLReports: 1507 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1508 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1509 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1510 1511 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1512 1513 return infoText 1514 1515 def RequestTradingStatus(self) -> dict: 1516 """ 1517 Requesting trading status for the instrument defined by `figi` variable. 1518 1519 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1520 1521 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1522 1523 :return: dictionary with trading status attributes. Response example: 1524 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1525 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1526 """ 1527 if self._figi is None or not self._figi: 1528 uLogger.error("Variable `figi` must be defined for using this method!") 1529 raise Exception("FIGI required") 1530 1531 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1532 1533 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1534 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1535 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1536 1537 if self.moreDebug: 1538 uLogger.debug("Records about current trading status successfully received") 1539 1540 return tradingStatus 1541 1542 def RequestPortfolio(self) -> dict: 1543 """ 1544 Requesting actual user's portfolio for current `accountId`. 1545 1546 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1547 1548 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1549 1550 :return: dictionary with user's portfolio. 1551 """ 1552 if self.accountId is None or not self.accountId: 1553 uLogger.error("Variable `accountId` must be defined for using this method!") 1554 raise Exception("Account ID required") 1555 1556 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1557 1558 self.body = str({"accountId": self.accountId}) 1559 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1560 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1561 1562 if self.moreDebug: 1563 uLogger.debug("Records about user's portfolio successfully received") 1564 1565 return rawPortfolio 1566 1567 def RequestPositions(self) -> dict: 1568 """ 1569 Requesting open positions by currencies and instruments for current `accountId`. 1570 1571 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1572 1573 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1574 1575 :return: dictionary with open positions by instruments. 1576 """ 1577 if self.accountId is None or not self.accountId: 1578 uLogger.error("Variable `accountId` must be defined for using this method!") 1579 raise Exception("Account ID required") 1580 1581 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1582 1583 self.body = str({"accountId": self.accountId}) 1584 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1585 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1586 1587 if self.moreDebug: 1588 uLogger.debug("Records about current open positions successfully received") 1589 1590 return rawPositions 1591 1592 def RequestPendingOrders(self) -> list: 1593 """ 1594 Requesting current actual pending limit orders for current `accountId`. 1595 1596 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1597 1598 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1599 1600 :return: list of dictionaries with pending limit orders. 1601 """ 1602 if self.accountId is None or not self.accountId: 1603 uLogger.error("Variable `accountId` must be defined for using this method!") 1604 raise Exception("Account ID required") 1605 1606 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1607 1608 self.body = str({"accountId": self.accountId}) 1609 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1610 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1611 1612 if "orders" in rawResponse.keys(): 1613 rawOrders = rawResponse["orders"] 1614 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1615 1616 else: 1617 rawOrders = [] 1618 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1619 1620 return rawOrders 1621 1622 def RequestStopOrders(self) -> list: 1623 """ 1624 Requesting current actual stop orders for current `accountId`. 1625 1626 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1627 1628 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1629 1630 :return: list of dictionaries with stop orders. 1631 """ 1632 if self.accountId is None or not self.accountId: 1633 uLogger.error("Variable `accountId` must be defined for using this method!") 1634 raise Exception("Account ID required") 1635 1636 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1637 1638 self.body = str({"accountId": self.accountId}) 1639 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1640 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1641 1642 if "stopOrders" in rawResponse.keys(): 1643 rawStopOrders = rawResponse["stopOrders"] 1644 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1645 1646 else: 1647 rawStopOrders = [] 1648 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1649 1650 return rawStopOrders 1651 1652 def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict: 1653 """ 1654 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1655 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1656 and `overviewBondsCalendarFile` are defined then also save information to file. 1657 1658 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1659 many requests about the state of the portfolio, and then, based on the received data, a large number 1660 of calculation and statistics are collected. 1661 1662 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1663 :param details: how detailed should the information be? 1664 - `full` — shows full available information about portfolio status (by default), 1665 - `positions` — shows only open positions, 1666 - `orders` — shows only sections of open limits and stop orders. 1667 - `digest` — show a short digest of the portfolio status, 1668 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1669 - `calendar` — shows only the bonds calendar section (if these present in portfolio). 1670 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1671 :return: dictionary with client's raw portfolio and some statistics. 1672 """ 1673 if self.accountId is None or not self.accountId: 1674 uLogger.error("Variable `accountId` must be defined for using this method!") 1675 raise Exception("Account ID required") 1676 1677 view = { 1678 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1679 "headers": {}, # list of dictionaries, response headers without "positions" section 1680 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1681 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1682 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1683 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1684 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1685 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1686 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1687 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1688 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1689 }, 1690 "stat": { # --- some statistics calculated using "raw" sections: 1691 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1692 "availableRUB": 0., # available rubles (without other currencies) 1693 "blockedRUB": 0., # blocked sum in Russian Rouble 1694 "totalChangesRUB": 0., # changes for all open trades in RUB 1695 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1696 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1697 "sharesCostRUB": 0., # costs of all shares in RUB 1698 "bondsCostRUB": 0., # costs of all bonds in RUB 1699 "etfsCostRUB": 0., # costs of all etfs in RUB 1700 "futuresCostRUB": 0., # costs of all futures in RUB 1701 "Currencies": [], # list of dictionaries of all currencies statistics 1702 "Shares": [], # list of dictionaries of all shares statistics 1703 "Bonds": [], # list of dictionaries of all bonds statistics 1704 "Etfs": [], # list of dictionaries of all etfs statistics 1705 "Futures": [], # list of dictionaries of all futures statistics 1706 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1707 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1708 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1709 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1710 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1711 }, 1712 "analytics": { # --- some analytics of portfolio: 1713 "distrByAssets": {}, # portfolio distribution by assets 1714 "distrByCompanies": {}, # portfolio distribution by companies 1715 "distrBySectors": {}, # portfolio distribution by sectors 1716 "distrByCurrencies": {}, # portfolio distribution by currencies 1717 "distrByCountries": {}, # portfolio distribution by countries 1718 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1719 } 1720 } 1721 1722 details = details.lower() 1723 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1724 if details not in availableDetails: 1725 details = "full" 1726 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1727 1728 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1729 1730 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1731 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1732 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1733 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1734 1735 # save response headers without "positions" section: 1736 for key in portfolioResponse.keys(): 1737 if key != "positions": 1738 view["raw"]["headers"][key] = portfolioResponse[key] 1739 1740 else: 1741 continue 1742 1743 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1744 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1745 for item in portfolioResponse["positions"]: 1746 if item["instrumentType"] == "currency": 1747 self._figi = item["figi"] 1748 if not self._figi and item["ticker"]: 1749 self._ticker = item["ticker"] 1750 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1751 1752 curr = self.SearchByFIGI(requestPrice=False) 1753 1754 # current price of currency in RUB: 1755 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1756 "name": curr["name"], 1757 "currentPrice": NanoToFloat( 1758 item["currentPrice"]["units"], 1759 item["currentPrice"]["nano"] 1760 ), 1761 } 1762 1763 view["raw"]["Currencies"].append(item) 1764 1765 elif item["instrumentType"] == "share": 1766 view["raw"]["Shares"].append(item) 1767 1768 elif item["instrumentType"] == "bond": 1769 view["raw"]["Bonds"].append(item) 1770 1771 elif item["instrumentType"] == "etf": 1772 view["raw"]["Etfs"].append(item) 1773 1774 elif item["instrumentType"] == "futures": 1775 view["raw"]["Futures"].append(item) 1776 1777 else: 1778 continue 1779 1780 # how many volume of currencies (by ISO currency name) are blocked: 1781 for item in view["raw"]["positions"]["blocked"]: 1782 blocked = NanoToFloat(item["units"], item["nano"]) 1783 if blocked > 0: 1784 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1785 1786 # how many volume of instruments (by FIGI) are blocked: 1787 for item in view["raw"]["positions"]["securities"]: 1788 blocked = int(item["blocked"]) 1789 if blocked > 0: 1790 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1791 1792 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1793 1794 if "rub" in allBlocked.keys(): 1795 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1796 1797 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1798 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1799 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1800 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1801 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1802 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1803 view["stat"]["portfolioCostRUB"] = sum([ 1804 view["stat"]["allCurrenciesCostRUB"], 1805 view["stat"]["sharesCostRUB"], 1806 view["stat"]["bondsCostRUB"], 1807 view["stat"]["etfsCostRUB"], 1808 view["stat"]["futuresCostRUB"], 1809 ]) 1810 1811 # --- calculating some portfolio statistics: 1812 byComp = {} # distribution by companies 1813 bySect = {} # distribution by sectors 1814 byCurr = {} # distribution by currencies (include RUB) 1815 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1816 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1817 1818 for item in portfolioResponse["positions"]: 1819 self._figi = item["figi"] 1820 if not self._figi and item["ticker"]: 1821 self._ticker = item["ticker"] 1822 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1823 1824 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1825 1826 if instrument: 1827 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1828 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1829 1830 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1831 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1832 1833 else: 1834 blocked = 0 1835 1836 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1837 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1838 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1839 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1840 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1841 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1842 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1843 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1844 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1845 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1846 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1847 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1848 1849 statData = { 1850 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1851 "ticker": instrument["ticker"], # ticker by FIGI 1852 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1853 "volume": volume, # available volume of instrument 1854 "lots": lots, # volume in lots of instrument 1855 "direction": direction, # direction of an instrument's position: short or long 1856 "blocked": blocked, # blocked volume of currency or instrument 1857 "currentPrice": curPrice, # current instrument's price in basic asset 1858 "average": average, # current average position price 1859 "cost": cost, # current cost of all volume of instrument in basic asset 1860 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1861 "costRUB": costRUB, # cost of instrument in ruble 1862 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1863 "profit": profit, # expected profit at current moment 1864 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1865 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1866 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1867 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1868 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1869 "step": instrument["step"], # minimum price increment 1870 } 1871 1872 # adding distribution by unique countries: 1873 if statData["country"] not in byCountry.keys(): 1874 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1875 1876 else: 1877 byCountry[statData["country"]]["cost"] += costRUB 1878 byCountry[statData["country"]]["percent"] += percentCostRUB 1879 1880 if item["instrumentType"] != "currency": 1881 # adding distribution by unique companies: 1882 if statData["name"]: 1883 if statData["name"] not in byComp.keys(): 1884 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1885 1886 else: 1887 byComp[statData["name"]]["cost"] += costRUB 1888 byComp[statData["name"]]["percent"] += percentCostRUB 1889 1890 # adding distribution by unique sectors: 1891 if statData["sector"] not in bySect.keys(): 1892 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1893 1894 else: 1895 bySect[statData["sector"]]["cost"] += costRUB 1896 bySect[statData["sector"]]["percent"] += percentCostRUB 1897 1898 # adding distribution by unique currencies: 1899 if currency not in byCurr.keys(): 1900 byCurr[currency] = { 1901 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1902 "cost": costRUB, 1903 "percent": percentCostRUB 1904 } 1905 1906 else: 1907 byCurr[currency]["cost"] += costRUB 1908 byCurr[currency]["percent"] += percentCostRUB 1909 1910 # saving statistics for every instrument: 1911 if item["instrumentType"] == "currency": 1912 view["stat"]["Currencies"].append(statData) 1913 1914 # update dict with free funds for trading (total - blocked) by currencies 1915 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1916 view["stat"]["funds"][currency] = { 1917 "total": volume, 1918 "totalCostRUB": costRUB, # total volume cost in rubles 1919 "free": volume - blocked, 1920 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1921 } 1922 1923 elif item["instrumentType"] == "share": 1924 view["stat"]["Shares"].append(statData) 1925 1926 elif item["instrumentType"] == "bond": 1927 view["stat"]["Bonds"].append(statData) 1928 1929 elif item["instrumentType"] == "etf": 1930 view["stat"]["Etfs"].append(statData) 1931 1932 elif item["instrumentType"] == "Futures": 1933 view["stat"]["Futures"].append(statData) 1934 1935 else: 1936 continue 1937 1938 # total changes in Russian Ruble: 1939 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1940 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1941 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1942 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1943 view["stat"]["funds"]["rub"] = { 1944 "total": view["stat"]["availableRUB"], 1945 "totalCostRUB": view["stat"]["availableRUB"], 1946 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1947 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1948 } 1949 1950 # --- pending limit orders sector data: 1951 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1952 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1953 1954 for item in view["raw"]["orders"]: 1955 self._figi = item["figi"] 1956 1957 if item["figi"] not in uniquePendingOrdersFIGIs: 1958 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1959 1960 uniquePendingOrdersFIGIs.append(item["figi"]) 1961 uniquePendingOrders[item["figi"]] = instrument 1962 1963 else: 1964 instrument = uniquePendingOrders[item["figi"]] 1965 1966 if instrument: 1967 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1968 orderType = TKS_ORDER_TYPES[item["orderType"]] 1969 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1970 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1971 1972 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1973 if item["direction"] == "ORDER_DIRECTION_BUY": 1974 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1975 1976 else: 1977 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1978 1979 # requested price for order execution: 1980 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1981 1982 # necessary changes in percent to reach target from current price: 1983 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1984 1985 view["stat"]["orders"].append({ 1986 "orderID": item["orderId"], # orderId number parameter of current order 1987 "figi": item["figi"], # FIGI identification 1988 "ticker": instrument["ticker"], # ticker name by FIGI 1989 "lotsRequested": item["lotsRequested"], # requested lots value 1990 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1991 "currentPrice": lastPrice, # current instrument's price for defined action 1992 "targetPrice": target, # requested price for order execution in base currency 1993 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1994 "percentChanges": changes, # changes in percent to target from current price 1995 "currency": item["currency"], # instrument's currency name 1996 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1997 "type": orderType, # type of order from TKS_ORDER_TYPES 1998 "status": orderState, # order status from TKS_ORDER_STATES 1999 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 2000 }) 2001 2002 # --- stop orders sector data: 2003 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 2004 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 2005 2006 for item in view["raw"]["stopOrders"]: 2007 self._figi = item["figi"] 2008 2009 if item["figi"] not in uniqueStopOrdersFIGIs: 2010 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 2011 2012 uniqueStopOrdersFIGIs.append(item["figi"]) 2013 uniqueStopOrders[item["figi"]] = instrument 2014 2015 else: 2016 instrument = uniqueStopOrders[item["figi"]] 2017 2018 if instrument: 2019 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 2020 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 2021 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 2022 2023 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 2024 if "expirationTime" in item.keys(): 2025 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 2026 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 2027 2028 else: 2029 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 2030 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 2031 2032 # current instrument's price (last sellers order if buy, and last buyers order if sell): 2033 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 2034 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2035 2036 else: 2037 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2038 2039 # requested price when stop-order executed: 2040 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2041 2042 # price for limit-order, set up when stop-order executed: 2043 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2044 2045 # necessary changes in percent to reach target from current price: 2046 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2047 2048 view["stat"]["stopOrders"].append({ 2049 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2050 "figi": item["figi"], # FIGI identification 2051 "ticker": instrument["ticker"], # ticker name by FIGI 2052 "lotsRequested": item["lotsRequested"], # requested lots value 2053 "currentPrice": lastPrice, # current instrument's price for defined action 2054 "targetPrice": target, # requested price for stop-order execution in base currency 2055 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2056 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2057 "percentChanges": changes, # changes in percent to target from current price 2058 "currency": item["currency"], # instrument's currency name 2059 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2060 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2061 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2062 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2063 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2064 }) 2065 2066 # --- calculating data for analytics section: 2067 # portfolio distribution by assets: 2068 view["analytics"]["distrByAssets"] = { 2069 "Ruble": { 2070 "uniques": 1, 2071 "cost": view["stat"]["availableRUB"], 2072 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2073 }, 2074 "Currencies": { 2075 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2076 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2077 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2078 }, 2079 "Shares": { 2080 "uniques": len(view["stat"]["Shares"]), 2081 "cost": view["stat"]["sharesCostRUB"], 2082 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2083 }, 2084 "Bonds": { 2085 "uniques": len(view["stat"]["Bonds"]), 2086 "cost": view["stat"]["bondsCostRUB"], 2087 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2088 }, 2089 "Etfs": { 2090 "uniques": len(view["stat"]["Etfs"]), 2091 "cost": view["stat"]["etfsCostRUB"], 2092 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2093 }, 2094 "Futures": { 2095 "uniques": len(view["stat"]["Futures"]), 2096 "cost": view["stat"]["futuresCostRUB"], 2097 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2098 }, 2099 } 2100 2101 # portfolio distribution by companies: 2102 view["analytics"]["distrByCompanies"]["All money cash"] = { 2103 "ticker": "", 2104 "cost": view["stat"]["allCurrenciesCostRUB"], 2105 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2106 } 2107 view["analytics"]["distrByCompanies"].update(byComp) 2108 2109 # portfolio distribution by sectors: 2110 view["analytics"]["distrBySectors"]["All money cash"] = { 2111 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2112 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2113 } 2114 view["analytics"]["distrBySectors"].update(bySect) 2115 2116 # portfolio distribution by currencies: 2117 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2118 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2119 2120 if self.moreDebug: 2121 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2122 2123 view["analytics"]["distrByCurrencies"].update(byCurr) 2124 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2125 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2126 2127 # portfolio distribution by countries: 2128 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2129 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2130 2131 if self.moreDebug: 2132 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2133 2134 view["analytics"]["distrByCountries"].update(byCountry) 2135 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2136 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2137 2138 # --- Prepare text statistics overview in human-readable: 2139 if show or onlyFiles: 2140 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2141 2142 # Whatever the value `details`, header not changes: 2143 info = [ 2144 "# Client's portfolio\n\n", 2145 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2146 "* **Account ID:** [{}]\n".format(self.accountId), 2147 ] 2148 2149 if details in ["full", "positions", "digest"]: 2150 info.extend([ 2151 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2152 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2153 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2154 view["stat"]["totalChangesRUB"], 2155 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2156 view["stat"]["totalChangesPercentRUB"], 2157 ), 2158 ]) 2159 2160 if details in ["full", "positions"]: 2161 info.extend([ 2162 "## Open positions\n\n", 2163 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2164 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2165 "| **Ruble:** | {:>31} | | | | | |\n".format( 2166 "{:.2f} ({:.2f}) rub".format( 2167 view["stat"]["availableRUB"], 2168 view["stat"]["blockedRUB"], 2169 ) 2170 ) 2171 ]) 2172 2173 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2174 return [ 2175 "| | | | | | | |\n", 2176 "| {:<27} | | | | | {:>19} | |\n".format( 2177 noTradeStr if noTradeStr else typeStr, 2178 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2179 ), 2180 ] 2181 2182 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2183 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2184 "{} [{}]".format(data["ticker"], data["figi"]), 2185 "{:.2f} ({:.2f}) {}".format( 2186 data["volume"], 2187 data["blocked"], 2188 data["currency"], 2189 ) if isCurr else "{:.0f} ({:.0f})".format( 2190 data["volume"], 2191 data["blocked"], 2192 ), 2193 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2194 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2195 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2196 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2197 "{}{:.2f} {} ({}{:.2f}%)".format( 2198 "+" if data["profit"] > 0 else "", 2199 data["profit"], data["baseCurrencyName"], 2200 "+" if data["percentProfit"] > 0 else "", 2201 data["percentProfit"], 2202 ), 2203 ) 2204 2205 # --- Show currencies section: 2206 if view["stat"]["Currencies"]: 2207 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2208 for item in view["stat"]["Currencies"]: 2209 info.append(_InfoStr(item, isCurr=True)) 2210 2211 else: 2212 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2213 2214 # --- Show shares section: 2215 if view["stat"]["Shares"]: 2216 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2217 2218 for item in view["stat"]["Shares"]: 2219 info.append(_InfoStr(item)) 2220 2221 else: 2222 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2223 2224 # --- Show bonds section: 2225 if view["stat"]["Bonds"]: 2226 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2227 2228 for item in view["stat"]["Bonds"]: 2229 info.append(_InfoStr(item)) 2230 2231 else: 2232 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2233 2234 # --- Show etfs section: 2235 if view["stat"]["Etfs"]: 2236 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2237 2238 for item in view["stat"]["Etfs"]: 2239 info.append(_InfoStr(item)) 2240 2241 else: 2242 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2243 2244 # --- Show futures section: 2245 if view["stat"]["Futures"]: 2246 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2247 2248 for item in view["stat"]["Futures"]: 2249 info.append(_InfoStr(item)) 2250 2251 else: 2252 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2253 2254 if details in ["full", "orders"]: 2255 # --- Show pending limit orders section: 2256 if view["stat"]["orders"]: 2257 info.extend([ 2258 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2259 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2260 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2261 ]) 2262 2263 for item in view["stat"]["orders"]: 2264 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2265 "{} [{}]".format(item["ticker"], item["figi"]), 2266 item["orderID"], 2267 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2268 "{} {} ({}{:.2f}%)".format( 2269 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2270 item["baseCurrencyName"], 2271 "+" if item["percentChanges"] > 0 else "", 2272 float(item["percentChanges"]), 2273 ), 2274 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2275 item["action"], 2276 item["type"], 2277 item["date"], 2278 )) 2279 2280 else: 2281 info.append("\n## Total pending limit-orders: [0]\n") 2282 2283 # --- Show stop orders section: 2284 if view["stat"]["stopOrders"]: 2285 info.extend([ 2286 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2287 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2288 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2289 ]) 2290 2291 for item in view["stat"]["stopOrders"]: 2292 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2293 "{} [{}]".format(item["ticker"], item["figi"]), 2294 item["orderID"], 2295 item["lotsRequested"], 2296 "{} {} ({}{:.2f}%)".format( 2297 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2298 item["baseCurrencyName"], 2299 "+" if item["percentChanges"] > 0 else "", 2300 float(item["percentChanges"]), 2301 ), 2302 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2303 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2304 item["action"], 2305 item["type"], 2306 item["expType"], 2307 item["createDate"], 2308 item["expDate"], 2309 )) 2310 2311 else: 2312 info.append("\n## Total stop-orders: [0]\n") 2313 2314 if details in ["full", "analytics"]: 2315 # -- Show analytics section: 2316 if view["stat"]["portfolioCostRUB"] > 0: 2317 info.extend([ 2318 "\n# Analytics\n\n" 2319 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2320 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2321 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2322 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2323 view["stat"]["totalChangesRUB"], 2324 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2325 view["stat"]["totalChangesPercentRUB"], 2326 ), 2327 "\n## Portfolio distribution by assets\n" 2328 "\n| Type | Uniques | Percent | Current cost |\n", 2329 "|------------------------------------|---------|---------|--------------------|\n", 2330 ]) 2331 2332 for key in view["analytics"]["distrByAssets"].keys(): 2333 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2334 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2335 key, 2336 view["analytics"]["distrByAssets"][key]["uniques"], 2337 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2338 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2339 )) 2340 2341 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2342 2343 info.extend([ 2344 "\n## Portfolio distribution by companies\n" 2345 "\n| Company | Percent | Current cost |\n", 2346 aSepLine, 2347 ]) 2348 2349 for company in view["analytics"]["distrByCompanies"].keys(): 2350 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2351 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2352 "{}{}".format( 2353 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2354 company, 2355 ), 2356 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2357 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2358 )) 2359 2360 info.extend([ 2361 "\n## Portfolio distribution by sectors\n" 2362 "\n| Sector | Percent | Current cost |\n", 2363 aSepLine, 2364 ]) 2365 2366 for sector in view["analytics"]["distrBySectors"].keys(): 2367 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2368 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2369 sector, 2370 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2371 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2372 )) 2373 2374 info.extend([ 2375 "\n## Portfolio distribution by currencies\n" 2376 "\n| Instruments currencies | Percent | Current cost |\n", 2377 aSepLine, 2378 ]) 2379 2380 for curr in view["analytics"]["distrByCurrencies"].keys(): 2381 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2382 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2383 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2384 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2385 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2386 )) 2387 2388 info.extend([ 2389 "\n## Portfolio distribution by countries\n" 2390 "\n| Assets by country | Percent | Current cost |\n", 2391 aSepLine, 2392 ]) 2393 2394 for country in view["analytics"]["distrByCountries"].keys(): 2395 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2396 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2397 country, 2398 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2399 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2400 )) 2401 2402 if details in ["full", "calendar"]: 2403 # -- Show bonds payment calendar section: 2404 if view["stat"]["Bonds"]: 2405 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2406 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2407 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2408 2409 else: 2410 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2411 2412 infoText = "".join(info) 2413 2414 if show and not onlyFiles: 2415 uLogger.info(infoText) 2416 2417 if details == "full" and self.overviewFile: 2418 filename = self.overviewFile 2419 2420 elif details == "digest" and self.overviewDigestFile: 2421 filename = self.overviewDigestFile 2422 2423 elif details == "positions" and self.overviewPositionsFile: 2424 filename = self.overviewPositionsFile 2425 2426 elif details == "orders" and self.overviewOrdersFile: 2427 filename = self.overviewOrdersFile 2428 2429 elif details == "analytics" and self.overviewAnalyticsFile: 2430 filename = self.overviewAnalyticsFile 2431 2432 elif details == "calendar" and self.overviewBondsCalendarFile: 2433 filename = self.overviewBondsCalendarFile 2434 2435 else: 2436 filename = "" 2437 2438 if filename and (show or onlyFiles): 2439 with open(filename, "w", encoding="UTF-8") as fH: 2440 fH.write(infoText) 2441 2442 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2443 2444 if self.useHTMLReports: 2445 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2446 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2447 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2448 2449 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2450 2451 return view 2452 2453 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]: 2454 """ 2455 Returns history operations between two given dates for current `accountId`. 2456 If `reportFile` string is not empty then also save human-readable report. 2457 Shows some statistical data of closed positions. 2458 2459 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2460 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2461 :param show: if `True` then also prints all records to the console. 2462 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2463 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2464 :return: original list of dictionaries with history of deals records from API ("operations" key): 2465 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2466 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2467 """ 2468 if self.accountId is None or not self.accountId: 2469 uLogger.error("Variable `accountId` must be defined for using this method!") 2470 raise Exception("Account ID required") 2471 2472 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2473 2474 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2475 2476 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2477 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2478 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2479 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2480 customStat = {} # custom statistics in additional to responseJSON 2481 2482 # --- output report in human-readable format: 2483 if self.reportFile and (show or onlyFiles): 2484 splitLine1 = "| | | | | |\n" # Summary section 2485 splitLine2 = "| | | | | | | | |\n" # Operations section 2486 nextDay = "" 2487 2488 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2489 2490 if len(ops) > 0: 2491 customStat = { 2492 "opsCount": 0, # total operations count 2493 "buyCount": 0, # buy operations 2494 "sellCount": 0, # sell operations 2495 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2496 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2497 "payIn": {"rub": 0.}, # Deposit brokerage account 2498 "payOut": {"rub": 0.}, # Withdrawals 2499 "divs": {"rub": 0.}, # Dividends income 2500 "coupons": {"rub": 0.}, # Coupon's income 2501 "brokerCom": {"rub": 0.}, # Service commissions 2502 "serviceCom": {"rub": 0.}, # Service commissions 2503 "marginCom": {"rub": 0.}, # Margin commissions 2504 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2505 } 2506 2507 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2508 for item in ops: 2509 if item["state"] == "OPERATION_STATE_EXECUTED": 2510 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2511 2512 # count buy operations: 2513 if "_BUY" in item["operationType"]: 2514 customStat["buyCount"] += 1 2515 2516 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2517 customStat["buyTotal"][item["payment"]["currency"]] += payment 2518 2519 else: 2520 customStat["buyTotal"][item["payment"]["currency"]] = payment 2521 2522 # count sell operations: 2523 elif "_SELL" in item["operationType"]: 2524 customStat["sellCount"] += 1 2525 2526 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2527 customStat["sellTotal"][item["payment"]["currency"]] += payment 2528 2529 else: 2530 customStat["sellTotal"][item["payment"]["currency"]] = payment 2531 2532 # count incoming operations: 2533 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2534 if item["payment"]["currency"] in customStat["payIn"].keys(): 2535 customStat["payIn"][item["payment"]["currency"]] += payment 2536 2537 else: 2538 customStat["payIn"][item["payment"]["currency"]] = payment 2539 2540 # count withdrawals operations: 2541 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2542 if item["payment"]["currency"] in customStat["payOut"].keys(): 2543 customStat["payOut"][item["payment"]["currency"]] += payment 2544 2545 else: 2546 customStat["payOut"][item["payment"]["currency"]] = payment 2547 2548 # count dividends income: 2549 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2550 if item["payment"]["currency"] in customStat["divs"].keys(): 2551 customStat["divs"][item["payment"]["currency"]] += payment 2552 2553 else: 2554 customStat["divs"][item["payment"]["currency"]] = payment 2555 2556 # count coupon's income: 2557 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2558 if item["payment"]["currency"] in customStat["coupons"].keys(): 2559 customStat["coupons"][item["payment"]["currency"]] += payment 2560 2561 else: 2562 customStat["coupons"][item["payment"]["currency"]] = payment 2563 2564 # count broker commissions: 2565 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2566 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2567 customStat["brokerCom"][item["payment"]["currency"]] += payment 2568 2569 else: 2570 customStat["brokerCom"][item["payment"]["currency"]] = payment 2571 2572 # count service commissions: 2573 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2574 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2575 customStat["serviceCom"][item["payment"]["currency"]] += payment 2576 2577 else: 2578 customStat["serviceCom"][item["payment"]["currency"]] = payment 2579 2580 # count margin commissions: 2581 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2582 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2583 customStat["marginCom"][item["payment"]["currency"]] += payment 2584 2585 else: 2586 customStat["marginCom"][item["payment"]["currency"]] = payment 2587 2588 # count withholding taxes: 2589 elif "_TAX" in item["operationType"]: 2590 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2591 customStat["allTaxes"][item["payment"]["currency"]] += payment 2592 2593 else: 2594 customStat["allTaxes"][item["payment"]["currency"]] = payment 2595 2596 else: 2597 continue 2598 2599 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2600 2601 # --- view "Actions" lines: 2602 info.extend([ 2603 "| Report sections | | | | |\n", 2604 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2605 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2606 "| | Buy: {:<22} | {:<28} | | |\n".format( 2607 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2608 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2609 ), 2610 "| | Sell: {:<21} | {:<28} | | |\n".format( 2611 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2612 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2613 ), 2614 ]) 2615 2616 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2617 for key in opsKeys: 2618 if key == "rub": 2619 continue 2620 2621 info.extend([ 2622 "| | | {:<28} | | |\n".format( 2623 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2624 ), 2625 "| | | {:<28} | | |\n".format( 2626 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2627 ), 2628 ]) 2629 2630 info.append(splitLine1) 2631 2632 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2633 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2634 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2635 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2636 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2637 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2638 ) 2639 2640 # --- view "Payments" lines: 2641 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2642 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2643 2644 for key in paymentsKeys: 2645 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2646 2647 info.append(splitLine1) 2648 2649 # --- view "Commissions and taxes" lines: 2650 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2651 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2652 2653 for key in comKeys: 2654 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2655 2656 info.extend([ 2657 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2658 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2659 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2660 ]) 2661 2662 else: 2663 info.append("Broker returned no operations during this period\n") 2664 2665 # --- view "Operations" section: 2666 for item in ops: 2667 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2668 continue 2669 2670 else: 2671 self._figi = item["figi"] 2672 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2673 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2674 2675 # group of deals during one day: 2676 if nextDay and item["date"].split("T")[0] != nextDay: 2677 info.append(splitLine2) 2678 nextDay = "" 2679 2680 else: 2681 nextDay = item["date"].split("T")[0] # saving current day for splitting 2682 2683 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2684 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2685 self._figi if self._figi else "—", 2686 instrument["ticker"] if instrument else "—", 2687 instrument["type"] if instrument else "—", 2688 item["quantity"] if int(item["quantity"]) > 0 else "—", 2689 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2690 TKS_OPERATION_STATES[item["state"]], 2691 TKS_OPERATION_TYPES[item["operationType"]], 2692 )) 2693 2694 infoText = "".join(info) 2695 2696 if show and not onlyFiles: 2697 if self.moreDebug: 2698 uLogger.debug("Records about history of a client's operations successfully received") 2699 2700 uLogger.info(infoText) 2701 2702 if self.reportFile and (show or onlyFiles): 2703 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2704 fH.write(infoText) 2705 2706 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2707 2708 if self.useHTMLReports: 2709 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2710 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2711 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2712 2713 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2714 2715 return ops, customStat 2716 2717 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame: 2718 """ 2719 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2720 2721 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2722 Warning! Broker server used ISO UTC time by default. 2723 2724 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2725 Also, `historyFile` used to update history with `onlyMissing` parameter. 2726 2727 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2728 2729 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2730 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2731 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2732 `"hour"`, `"day"`. Default: `"hour"`. 2733 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2734 False by default. Warning! History appends only from last candle to current time 2735 with always update last candle! 2736 :param csvSep: separator if csv-file is used, `,` by default. 2737 :param show: if `True` then also prints Pandas DataFrame to the console. 2738 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2739 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2740 `["date", "time", "open", "high", "low", "close", "volume"]`. 2741 """ 2742 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2743 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2744 history = None # empty pandas object for history 2745 2746 if interval not in TKS_CANDLE_INTERVALS.keys(): 2747 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2748 raise Exception("Incorrect value") 2749 2750 if not (self._ticker or self._figi): 2751 uLogger.error("Ticker or FIGI must be defined!") 2752 raise Exception("Ticker or FIGI required") 2753 2754 if self._ticker and not self._figi: 2755 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2756 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2757 2758 if self._figi and not self._ticker: 2759 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2760 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2761 2762 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2763 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2764 if interval.lower() != "day": 2765 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2766 2767 delta = dtEnd - dtStart # current UTC time minus last time in file 2768 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2769 2770 # calculate history length in candles: 2771 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2772 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2773 length += 1 # to avoid fraction time 2774 2775 # calculate data blocks count: 2776 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2777 2778 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2779 if self.moreDebug: 2780 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2781 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2782 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2783 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2784 2785 tempOld = None # pandas object for old history, if --only-missing key present 2786 lastTime = None # datetime object of last old candle in file 2787 2788 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2789 if self.moreDebug: 2790 uLogger.debug("--only-missing key present, add only last missing candles...") 2791 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2792 2793 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2794 2795 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2796 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2797 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2798 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2799 2800 # get last datetime object from last string in file or minus 1 delta if file is empty: 2801 if len(tempOld) > 0: 2802 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2803 2804 else: 2805 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2806 2807 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2808 2809 responseJSONs = [] # raw history blocks of data 2810 2811 blockEnd = dtEnd 2812 for item in range(blocks): 2813 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2814 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2815 2816 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2817 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2818 )) 2819 2820 if blockStart == blockEnd: 2821 uLogger.debug("Skipped this zero-length block...") 2822 2823 else: 2824 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2825 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2826 self.body = str({ 2827 "figi": self._figi, 2828 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2829 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2830 "interval": TKS_CANDLE_INTERVALS[interval][0] 2831 }) 2832 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2833 2834 if "code" in responseJSON.keys(): 2835 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2836 2837 else: 2838 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2839 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2840 2841 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2842 2843 blockEnd = blockStart 2844 2845 printCount = len(responseJSONs) # candles to show in console 2846 if responseJSONs: 2847 tempHistory = pd.DataFrame( 2848 data={ 2849 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2850 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2851 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2852 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2853 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2854 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2855 "volume": [int(item["volume"]) for item in responseJSONs], 2856 }, 2857 index=range(len(responseJSONs)), 2858 columns=["date", "time", "open", "high", "low", "close", "volume"], 2859 ) 2860 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2861 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2862 2863 # append only newest candles to old history if --only-missing key present: 2864 if onlyMissing and tempOld is not None and lastTime is not None: 2865 index = 0 # find start index in tempHistory data: 2866 2867 for i, item in tempHistory.iterrows(): 2868 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2869 2870 if curTime == lastTime: 2871 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2872 index = i 2873 printCount = index + 1 2874 break 2875 2876 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2877 2878 else: 2879 history = tempHistory # if no `--only-missing` key then load full data from server 2880 2881 if self.moreDebug: 2882 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2883 2884 if history is not None and not history.empty: 2885 if show and not onlyFiles: 2886 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2887 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2888 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2889 )) 2890 2891 else: 2892 uLogger.warning("Received an empty candles history!") 2893 2894 if self.historyFile is not None: 2895 if history is not None and not history.empty: 2896 history.to_csv(self.historyFile, sep=csvSep, index=False, header=False) 2897 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2898 2899 else: 2900 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2901 2902 else: 2903 if self.moreDebug: 2904 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2905 2906 return history 2907 2908 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2909 """ 2910 Load candles history from csv-file and return Pandas DataFrame object. 2911 2912 See also: `History()` and `ShowHistoryChart()` methods. 2913 2914 :param filePath: path to csv-file to open. 2915 """ 2916 loadedHistory = None # init candles data object 2917 2918 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2919 2920 if os.path.exists(filePath): 2921 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2922 2923 tfStr = self.priceModel.FormattedDelta( 2924 self.priceModel.timeframe, 2925 "{days} days {hours}h {minutes}m {seconds}s", 2926 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2927 self.priceModel.timeframe, 2928 "{hours}h {minutes}m {seconds}s", 2929 ) 2930 2931 if loadedHistory is not None and not loadedHistory.empty: 2932 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2933 len(loadedHistory), 2934 tfStr, 2935 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2936 ) 2937 2938 else: 2939 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2940 2941 else: 2942 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2943 2944 return loadedHistory 2945 2946 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2947 """ 2948 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2949 2950 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2951 Default: `index.html` (both for interact and non-interact candlesticks chart). 2952 2953 See also: `History()` and `LoadHistory()` methods. 2954 2955 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2956 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2957 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2958 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2959 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2960 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2961 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2962 """ 2963 if isinstance(candles, str): 2964 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2965 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2966 2967 elif isinstance(candles, pd.DataFrame): 2968 self.priceModel.prices = candles # set candles chain from variable 2969 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2970 2971 if "datetime" not in candles.columns: 2972 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2973 2974 else: 2975 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2976 raise Exception("Incorrect value") 2977 2978 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2979 2980 if interact: 2981 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2982 2983 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2984 2985 else: 2986 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2987 2988 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2989 2990 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2991 2992 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2993 """ 2994 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2995 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2996 2997 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2998 2999 :param operation: string "Buy" or "Sell". 3000 :param lots: volume, integer count of lots >= 1. 3001 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 3002 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 3003 :param expDate: string "Undefined" by default or local date in future, 3004 it is a string with format `%Y-%m-%d %H:%M:%S`. 3005 :return: JSON with response from broker server. 3006 """ 3007 if self.accountId is None or not self.accountId: 3008 uLogger.error("Variable `accountId` must be defined for using this method!") 3009 raise Exception("Account ID required") 3010 3011 if operation is None or not operation or operation not in ("Buy", "Sell"): 3012 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3013 raise Exception("Incorrect value") 3014 3015 if lots is None or lots < 1: 3016 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 3017 lots = 1 3018 3019 if tp is None or tp < 0: 3020 tp = 0 3021 3022 if sl is None or sl < 0: 3023 sl = 0 3024 3025 if expDate is None or not expDate: 3026 expDate = "Undefined" 3027 3028 if not (self._ticker or self._figi): 3029 uLogger.error("Ticker or FIGI must be defined!") 3030 raise Exception("Ticker or FIGI required") 3031 3032 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3033 self._ticker = instrument["ticker"] 3034 self._figi = instrument["figi"] 3035 3036 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 3037 3038 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3039 self.body = str({ 3040 "figi": self._figi, 3041 "quantity": str(lots), 3042 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3043 "accountId": str(self.accountId), 3044 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3045 }) 3046 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3047 3048 if "orderId" in response.keys(): 3049 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3050 operation, response["orderId"], 3051 self._ticker, self._figi, lots, 3052 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3053 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3054 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3055 )) 3056 3057 if tp > 0: 3058 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3059 3060 if sl > 0: 3061 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3062 3063 else: 3064 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3065 3066 return response 3067 3068 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3069 """ 3070 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3071 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3072 3073 See also: `Order()` and `Trade()` docstrings. 3074 3075 :param lots: volume, integer count of lots >= 1. 3076 :param tp: float > 0, take profit price of stop-order. 3077 :param sl: float > 0, stop loss price of stop-order. 3078 :param expDate: it's a local date in future. 3079 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3080 :return: JSON with response from broker server. 3081 """ 3082 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3083 3084 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3085 """ 3086 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3087 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3088 3089 See also: `Order()` and `Trade()` docstrings. 3090 3091 :param lots: volume, integer count of lots >= 1. 3092 :param tp: float > 0, take profit price of stop-order. 3093 :param sl: float > 0, stop loss price of stop-order. 3094 :param expDate: it's a local date in the future. 3095 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3096 :return: JSON with response from broker server. 3097 """ 3098 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3099 3100 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3101 """ 3102 Close position of given instruments. 3103 3104 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3105 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3106 This avoids unnecessary downloading data from the server. 3107 """ 3108 if instruments is None or not instruments: 3109 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3110 raise Exception("Ticker or FIGI required") 3111 3112 if isinstance(instruments, str): 3113 instruments = [instruments] 3114 3115 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3116 if uniqueInstruments: 3117 if portfolio is None or not portfolio: 3118 portfolio = self.Overview(show=False) 3119 3120 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3121 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3122 3123 for self._figi in uniqueInstruments: 3124 if self._figi not in allOpened: 3125 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3126 continue 3127 3128 # search open trade info about instrument by ticker: 3129 instrument = {} 3130 for iType in TKS_INSTRUMENTS: 3131 if instrument: 3132 break 3133 3134 for item in portfolio["stat"][iType]: 3135 if item["figi"] == self._figi: 3136 instrument = item 3137 break 3138 3139 if instrument: 3140 self._ticker = instrument["ticker"] 3141 self._figi = instrument["figi"] 3142 3143 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3144 self._ticker, 3145 self._figi, 3146 int(instrument["volume"]), 3147 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3148 )) 3149 3150 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3151 3152 if tradeLots > 0: 3153 if instrument["blocked"] > 0: 3154 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3155 instrument["blocked"], 3156 self._ticker, 3157 tradeLots, 3158 )) 3159 3160 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3161 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3162 3163 else: 3164 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3165 3166 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3167 """ 3168 Close all positions of given instruments with defined type. 3169 3170 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3171 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3172 This avoids unnecessary downloading data from the server. 3173 """ 3174 if iType not in TKS_INSTRUMENTS: 3175 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3176 3177 else: 3178 if portfolio is None or not portfolio: 3179 portfolio = self.Overview(show=False) 3180 3181 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3182 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3183 3184 if tickers and portfolio: 3185 self.CloseTrades(tickers, portfolio) 3186 3187 else: 3188 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3189 3190 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3191 """ 3192 Universal method to create market or limit orders with all available parameters for current `accountId`. 3193 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3194 3195 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3196 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3197 3198 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3199 then broker immediately open market order as you can do simple --buy or --sell operations! 3200 3201 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3202 When current price will go up or down to target price value then broker opens a limit order. 3203 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3204 3205 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3206 3207 :param operation: string "Buy" or "Sell". 3208 :param orderType: string "Limit" or "Stop". 3209 :param lots: volume, integer count of lots >= 1. 3210 :param targetPrice: target price > 0. This is open trade price for limit order. 3211 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3212 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3213 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3214 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3215 Stop loss order always executed by market price. 3216 :param expDate: string "Undefined" by default or local date in future. 3217 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3218 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3219 A limit order has no expiration date, it lasts until the end of the trading day. 3220 :return: JSON with response from broker server. 3221 """ 3222 if self.accountId is None or not self.accountId: 3223 uLogger.error("Variable `accountId` must be defined for using this method!") 3224 raise Exception("Account ID required") 3225 3226 if operation is None or not operation or operation not in ("Buy", "Sell"): 3227 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3228 raise Exception("Incorrect value") 3229 3230 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3231 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3232 raise Exception("Incorrect value") 3233 3234 if lots is None or lots < 1: 3235 uLogger.error("You must define trade volume > 0: integer count of lots!") 3236 raise Exception("Incorrect value") 3237 3238 if targetPrice is None or targetPrice <= 0: 3239 uLogger.error("Target price for limit-order must be greater than 0!") 3240 raise Exception("Incorrect value") 3241 3242 if limitPrice is None or limitPrice <= 0: 3243 limitPrice = targetPrice 3244 3245 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3246 stopType = "Limit" 3247 3248 if expDate is None or not expDate: 3249 expDate = "Undefined" 3250 3251 if not (self._ticker or self._figi): 3252 uLogger.error("Tocker or FIGI must be defined!") 3253 raise Exception("Ticker or FIGI required") 3254 3255 response = {} 3256 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3257 self._ticker = instrument["ticker"] 3258 self._figi = instrument["figi"] 3259 3260 if orderType == "Limit": 3261 uLogger.debug( 3262 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3263 self._ticker, self._figi, 3264 operation, lots, targetPrice, instrument["currency"], 3265 )) 3266 3267 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3268 self.body = str({ 3269 "figi": self._figi, 3270 "quantity": str(lots), 3271 "price": FloatToNano(targetPrice), 3272 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3273 "accountId": str(self.accountId), 3274 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3275 }) 3276 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3277 3278 if "orderId" in response.keys(): 3279 uLogger.info( 3280 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3281 response["orderId"], self._ticker, self._figi, operation, lots, 3282 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3283 )) 3284 3285 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3286 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3287 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3288 targetPrice, instrument["currency"], 3289 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3290 )) 3291 3292 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3293 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3294 targetPrice, instrument["currency"], 3295 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3296 )) 3297 3298 else: 3299 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3300 3301 if orderType == "Stop": 3302 uLogger.debug( 3303 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3304 self._ticker, self._figi, 3305 operation, lots, 3306 targetPrice, instrument["currency"], 3307 limitPrice, instrument["currency"], 3308 stopType, expDate, 3309 )) 3310 3311 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3312 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3313 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3314 3315 body = { 3316 "figi": self._figi, 3317 "quantity": str(lots), 3318 "price": FloatToNano(limitPrice), 3319 "stopPrice": FloatToNano(targetPrice), 3320 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3321 "accountId": str(self.accountId), 3322 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3323 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3324 } 3325 3326 if expDateUTC: 3327 body["expireDate"] = expDateUTC 3328 3329 self.body = str(body) 3330 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3331 3332 if "stopOrderId" in response.keys(): 3333 uLogger.info( 3334 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3335 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3336 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3337 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3338 TKS_STOP_ORDER_TYPES[stopOrderType], 3339 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3340 )) 3341 3342 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3343 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3344 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{} {}] is lower than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3345 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3346 "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"], 3347 )) 3348 3349 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3350 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{} {}] is higher than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3351 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3352 "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"], 3353 )) 3354 3355 else: 3356 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3357 3358 return response 3359 3360 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3361 """ 3362 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3363 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3364 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3365 See also: `Order()` docstring. 3366 3367 :param lots: volume, integer count of lots >= 1. 3368 :param targetPrice: target price > 0. This is open trade price for limit order. 3369 :return: JSON with response from broker server. 3370 """ 3371 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3372 3373 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3374 """ 3375 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3376 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3377 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3378 target price value then broker opens a limit order. See also: `Order()` docstring. 3379 3380 :param lots: volume, integer count of lots >= 1. 3381 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3382 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3383 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3384 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3385 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3386 :param expDate: string "Undefined" by default or local date in future. 3387 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3388 This date is converting to UTC format for server. 3389 :return: JSON with response from broker server. 3390 """ 3391 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3392 3393 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3394 """ 3395 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3396 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3397 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3398 See also: `Order()` docstring. 3399 3400 :param lots: volume, integer count of lots >= 1. 3401 :param targetPrice: target price > 0. This is open trade price for limit order. 3402 :return: JSON with response from broker server. 3403 """ 3404 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3405 3406 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3407 """ 3408 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3409 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3410 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3411 target price value then broker opens a limit order. See also: `Order()` docstring. 3412 3413 :param lots: volume, integer count of lots >= 1. 3414 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3415 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3416 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3417 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3418 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3419 :param expDate: string "Undefined" by default or local date in future. 3420 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3421 This date is converting to UTC format for server. 3422 :return: JSON with response from broker server. 3423 """ 3424 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3425 3426 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3427 """ 3428 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3429 3430 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3431 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3432 This avoids unnecessary downloading data from the server. 3433 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3434 """ 3435 if self.accountId is None or not self.accountId: 3436 uLogger.error("Variable `accountId` must be defined for using this method!") 3437 raise Exception("Account ID required") 3438 3439 if orderIDs: 3440 if allOrdersIDs is None: 3441 rawOrders = self.RequestPendingOrders() 3442 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3443 3444 if allStopOrdersIDs is None: 3445 rawStopOrders = self.RequestStopOrders() 3446 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3447 3448 for orderID in orderIDs: 3449 idInPendingOrders = orderID in allOrdersIDs 3450 idInStopOrders = orderID in allStopOrdersIDs 3451 3452 if not (idInPendingOrders or idInStopOrders): 3453 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3454 continue 3455 3456 else: 3457 if idInPendingOrders: 3458 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3459 3460 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3461 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3462 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3463 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3464 3465 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3466 if self.moreDebug: 3467 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3468 3469 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3470 3471 else: 3472 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3473 3474 elif idInStopOrders: 3475 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3476 3477 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3478 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3479 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3480 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3481 3482 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3483 if self.moreDebug: 3484 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3485 3486 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3487 3488 else: 3489 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3490 3491 else: 3492 continue 3493 3494 def CloseAllOrders(self) -> None: 3495 """ 3496 Gets a list of open pending and stop orders and cancel it all. 3497 """ 3498 rawOrders = self.RequestPendingOrders() 3499 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3500 lenOrders = len(allOrdersIDs) 3501 3502 rawStopOrders = self.RequestStopOrders() 3503 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3504 lenSOrders = len(allStopOrdersIDs) 3505 3506 if lenOrders > 0 or lenSOrders > 0: 3507 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3508 3509 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3510 3511 else: 3512 uLogger.info("Orders not found, nothing to cancel.") 3513 3514 def CloseAll(self, *args) -> None: 3515 """ 3516 Close all available (not blocked) opened trades and orders. 3517 3518 Also, you can select one or more keywords case-insensitive: 3519 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3520 3521 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3522 """ 3523 overview = self.Overview(show=False) # get all open trades info 3524 3525 if len(args) == 0: 3526 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3527 self.CloseAllOrders() # close all pending and stop orders 3528 3529 for iType in TKS_INSTRUMENTS: 3530 if iType != "Currencies": 3531 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3532 3533 else: 3534 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3535 lowerArgs = [x.lower() for x in args] 3536 3537 if "orders" in lowerArgs: 3538 self.CloseAllOrders() # close all pending and stop orders 3539 3540 for iType in TKS_INSTRUMENTS: 3541 if iType.lower() in lowerArgs and iType != "Currencies": 3542 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3543 3544 def CloseAllByTicker(self, instrument: str) -> None: 3545 """ 3546 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3547 3548 This method searches opened trade and orders of instrument throw all portfolio and then use 3549 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3550 3551 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3552 3553 :param instrument: string with ticker. 3554 """ 3555 if instrument is None or not instrument: 3556 uLogger.error("Ticker name must be defined for using this method!") 3557 raise Exception("Ticker required") 3558 3559 overview = self.Overview(show=False) # get user portfolio with all open trades info 3560 3561 self._ticker = instrument # try to set instrument as ticker 3562 self._figi = "" 3563 3564 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3565 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3566 3567 if limitAll and self.IsInLimitOrders(portfolio=overview): 3568 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3569 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3570 3571 if stopAll and self.IsInStopOrders(portfolio=overview): 3572 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3573 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3574 3575 if self.IsInPortfolio(portfolio=overview): 3576 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3577 self.CloseTrades(instruments=[instrument], portfolio=overview) 3578 3579 def CloseAllByFIGI(self, instrument: str) -> None: 3580 """ 3581 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3582 3583 This method searches opened trade and orders of instrument throw all portfolio and then use 3584 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3585 3586 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3587 3588 :param instrument: string with FIGI id. 3589 """ 3590 if instrument is None or not instrument: 3591 uLogger.error("FIGI id must be defined for using this method!") 3592 raise Exception("FIGI required") 3593 3594 overview = self.Overview(show=False) # get user portfolio with all open trades info 3595 3596 self._ticker = "" 3597 self._figi = instrument # try to set instrument as FIGI id 3598 3599 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3600 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3601 3602 if limitAll and self.IsInLimitOrders(portfolio=overview): 3603 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3604 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3605 3606 if stopAll and self.IsInStopOrders(portfolio=overview): 3607 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3608 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3609 3610 if self.IsInPortfolio(portfolio=overview): 3611 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3612 self.CloseTrades(instruments=[instrument], portfolio=overview) 3613 3614 @staticmethod 3615 def ParseOrderParameters(operation, **inputParameters): 3616 """ 3617 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3618 3619 :param operation: string "Buy" or "Sell". 3620 :param inputParameters: this is dict of strings that looks like this 3621 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3622 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3623 "prices" key: one or more prices to open limit-orders 3624 Counts of values in lots and prices lists must be equals! 3625 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3626 """ 3627 # TODO: update order grid work with api v2 3628 pass 3629 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3630 # 3631 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3632 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3633 # raise Exception("Incorrect value") 3634 # 3635 # if "l" in inputParameters.keys(): 3636 # inputParameters["lots"] = inputParameters.pop("l") 3637 # 3638 # if "p" in inputParameters.keys(): 3639 # inputParameters["prices"] = inputParameters.pop("p") 3640 # 3641 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3642 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3643 # raise Exception("Incorrect value") 3644 # 3645 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3646 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3647 # 3648 # if len(lots) != len(prices): 3649 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3650 # raise Exception("Incorrect value") 3651 # 3652 # uLogger.debug("Extracted parameters for orders:") 3653 # uLogger.debug("lots = {}".format(lots)) 3654 # uLogger.debug("prices = {}".format(prices)) 3655 # 3656 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3657 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3658 # uLogger.debug("Order parameters: {}".format(result)) 3659 # 3660 # return result 3661 3662 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3663 """ 3664 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3665 3666 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3667 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3668 """ 3669 result = False 3670 msg = "Instrument not defined!" 3671 3672 if portfolio is None or not portfolio: 3673 portfolio = self.Overview(show=False) 3674 3675 if self._ticker: 3676 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3677 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3678 3679 for iType in TKS_INSTRUMENTS: 3680 for instrument in portfolio["stat"][iType]: 3681 if instrument["ticker"] == self._ticker: 3682 result = True 3683 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3684 break 3685 3686 elif self._figi: 3687 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3688 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3689 3690 for iType in TKS_INSTRUMENTS: 3691 for instrument in portfolio["stat"][iType]: 3692 if instrument["figi"] == self._figi: 3693 result = True 3694 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3695 break 3696 3697 else: 3698 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3699 3700 uLogger.debug(msg) 3701 3702 return result 3703 3704 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3705 """ 3706 Returns instrument from the user's portfolio if it presents there. 3707 Instrument must be defined by `ticker` (highly priority) or `figi`. 3708 3709 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3710 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3711 """ 3712 result = None 3713 msg = "Instrument not defined!" 3714 3715 if portfolio is None or not portfolio: 3716 portfolio = self.Overview(show=False) 3717 3718 if self._ticker: 3719 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3720 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3721 3722 for iType in TKS_INSTRUMENTS: 3723 for instrument in portfolio["stat"][iType]: 3724 if instrument["ticker"] == self._ticker: 3725 result = instrument 3726 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3727 break 3728 3729 elif self._figi: 3730 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3731 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3732 3733 for iType in TKS_INSTRUMENTS: 3734 for instrument in portfolio["stat"][iType]: 3735 if instrument["figi"] == self._figi: 3736 result = instrument 3737 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3738 break 3739 3740 else: 3741 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3742 3743 uLogger.debug(msg) 3744 3745 return result 3746 3747 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3748 """ 3749 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3750 3751 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3752 3753 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3754 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3755 """ 3756 result = False 3757 msg = "Instrument not defined!" 3758 3759 if portfolio is None or not portfolio: 3760 portfolio = self.Overview(show=False) 3761 3762 if self._ticker: 3763 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3764 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3765 3766 for instrument in portfolio["stat"]["orders"]: 3767 if instrument["ticker"] == self._ticker: 3768 result = True 3769 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3770 break 3771 3772 elif self._figi: 3773 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3774 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3775 3776 for instrument in portfolio["stat"]["orders"]: 3777 if instrument["figi"] == self._figi: 3778 result = True 3779 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3780 break 3781 3782 else: 3783 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3784 3785 uLogger.debug(msg) 3786 3787 return result 3788 3789 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3790 """ 3791 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3792 Instrument must be defined by `ticker` (highly priority) or `figi`. 3793 3794 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3795 3796 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3797 :return: list with `orderID`s of limit orders. 3798 """ 3799 result = [] 3800 msg = "Instrument not defined!" 3801 3802 if portfolio is None or not portfolio: 3803 portfolio = self.Overview(show=False) 3804 3805 if self._ticker: 3806 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3807 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3808 3809 for instrument in portfolio["stat"]["orders"]: 3810 if instrument["ticker"] == self._ticker: 3811 result.append(instrument["orderID"]) 3812 3813 if result: 3814 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3815 3816 elif self._figi: 3817 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3818 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3819 3820 for instrument in portfolio["stat"]["orders"]: 3821 if instrument["figi"] == self._figi: 3822 result.append(instrument["orderID"]) 3823 3824 if result: 3825 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3826 3827 else: 3828 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3829 3830 uLogger.debug(msg) 3831 3832 return result 3833 3834 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3835 """ 3836 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3837 3838 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3839 3840 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3841 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3842 """ 3843 result = False 3844 msg = "Instrument not defined!" 3845 3846 if portfolio is None or not portfolio: 3847 portfolio = self.Overview(show=False) 3848 3849 if self._ticker: 3850 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3851 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3852 3853 for instrument in portfolio["stat"]["stopOrders"]: 3854 if instrument["ticker"] == self._ticker: 3855 result = True 3856 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3857 break 3858 3859 elif self._figi: 3860 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3861 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3862 3863 for instrument in portfolio["stat"]["stopOrders"]: 3864 if instrument["figi"] == self._figi: 3865 result = True 3866 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3867 break 3868 3869 else: 3870 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3871 3872 uLogger.debug(msg) 3873 3874 return result 3875 3876 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3877 """ 3878 Returns list with all `orderID`s of opened stop orders for the instrument. 3879 Instrument must be defined by `ticker` (highly priority) or `figi`. 3880 3881 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3882 3883 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3884 :return: list with `orderID`s of stop orders. 3885 """ 3886 result = [] 3887 msg = "Instrument not defined!" 3888 3889 if portfolio is None or not portfolio: 3890 portfolio = self.Overview(show=False) 3891 3892 if self._ticker: 3893 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3894 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3895 3896 for instrument in portfolio["stat"]["stopOrders"]: 3897 if instrument["ticker"] == self._ticker: 3898 result.append(instrument["orderID"]) 3899 3900 if result: 3901 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3902 3903 elif self._figi: 3904 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3905 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3906 3907 for instrument in portfolio["stat"]["stopOrders"]: 3908 if instrument["figi"] == self._figi: 3909 result.append(instrument["orderID"]) 3910 3911 if result: 3912 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3913 3914 else: 3915 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3916 3917 uLogger.debug(msg) 3918 3919 return result 3920 3921 def RequestLimits(self) -> dict: 3922 """ 3923 Method for obtaining the available funds for withdrawal for current `accountId`. 3924 3925 See also: 3926 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3927 - `OverviewLimits()` method 3928 3929 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3930 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3931 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3932 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3933 """ 3934 if self.accountId is None or not self.accountId: 3935 uLogger.error("Variable `accountId` must be defined for using this method!") 3936 raise Exception("Account ID required") 3937 3938 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3939 3940 self.body = str({"accountId": self.accountId}) 3941 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3942 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3943 3944 if self.moreDebug: 3945 uLogger.debug("Records about available funds for withdrawal successfully received") 3946 3947 return rawLimits 3948 3949 def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict: 3950 """ 3951 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3952 3953 See also: `RequestLimits()`. 3954 3955 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3956 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 3957 :return: dict with raw parsed data from server and some calculated statistics about it. 3958 """ 3959 if self.accountId is None or not self.accountId: 3960 uLogger.error("Variable `accountId` must be defined for using this method!") 3961 raise Exception("Account ID required") 3962 3963 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3964 3965 view = { 3966 "rawLimits": rawLimits, 3967 "limits": { # parsed data for every currency: 3968 "money": { # this is an array of portfolio currency positions 3969 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3970 }, 3971 "blocked": { # this is an array of blocked currency 3972 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3973 }, 3974 "blockedGuarantee": { # this is locked money under collateral for futures 3975 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3976 }, 3977 }, 3978 } 3979 3980 # --- Prepare text table with limits in human-readable format: 3981 if show or onlyFiles: 3982 info = [ 3983 "# Withdrawal limits\n\n", 3984 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3985 "* **Account ID:** [{}]\n".format(self.accountId), 3986 ] 3987 3988 if view["limits"]["money"]: 3989 info.extend([ 3990 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3991 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3992 ]) 3993 3994 else: 3995 info.append("\nNo withdrawal limits\n") 3996 3997 for curr in view["limits"]["money"].keys(): 3998 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3999 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 4000 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 4001 4002 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 4003 "[{}]".format(curr), 4004 "{:.2f}".format(view["limits"]["money"][curr]), 4005 "{:.2f}".format(availableMoney), 4006 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 4007 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 4008 ) 4009 4010 if curr == "rub": 4011 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 4012 4013 else: 4014 info.append(infoStr) 4015 4016 infoText = "".join(info) 4017 4018 if show and not onlyFiles: 4019 uLogger.info(infoText) 4020 4021 if self.withdrawalLimitsFile and (show or onlyFiles): 4022 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 4023 fH.write(infoText) 4024 4025 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 4026 4027 if self.useHTMLReports: 4028 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 4029 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4030 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 4031 4032 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4033 4034 return view 4035 4036 def RequestAccounts(self) -> dict: 4037 """ 4038 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 4039 4040 See also: 4041 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 4042 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4043 - `OverviewUserInfo()` method 4044 4045 :return: dict with raw data from server that contains accounts info. Example of dict: 4046 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4047 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4048 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4049 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4050 """ 4051 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4052 4053 self.body = str({}) 4054 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4055 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4056 4057 if self.moreDebug: 4058 uLogger.debug("Records about available accounts successfully received") 4059 4060 return rawAccounts 4061 4062 def RequestUserInfo(self) -> dict: 4063 """ 4064 Method for requesting common user's information. 4065 4066 See also: 4067 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4068 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4069 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4070 - `OverviewUserInfo()` method 4071 4072 :return: dict with raw data from server that contains user's information. Example of dict: 4073 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4074 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4075 """ 4076 uLogger.debug("Requesting common user's information. Wait, please...") 4077 4078 self.body = str({}) 4079 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4080 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4081 4082 if self.moreDebug: 4083 uLogger.debug("Records about current user successfully received") 4084 4085 return rawUserInfo 4086 4087 def RequestMarginStatus(self, accountId: str = None) -> dict: 4088 """ 4089 Method for requesting margin calculation for defined account ID. 4090 4091 See also: 4092 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4093 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4094 - `OverviewUserInfo()` method 4095 4096 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4097 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4098 Example of responses: 4099 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4100 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4101 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4102 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4103 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4104 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4105 """ 4106 if accountId is None or not accountId: 4107 if self.accountId is None or not self.accountId: 4108 uLogger.error("Variable `accountId` must be defined for using this method!") 4109 raise Exception("Account ID required") 4110 4111 else: 4112 accountId = self.accountId # use `self.accountId` (main ID) by default 4113 4114 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4115 4116 self.body = str({"accountId": accountId}) 4117 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4118 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4119 4120 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4121 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4122 rawMargin = {} 4123 4124 else: 4125 if self.moreDebug: 4126 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4127 4128 return rawMargin 4129 4130 def RequestTariffLimits(self) -> dict: 4131 """ 4132 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4133 4134 See also: 4135 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4136 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4137 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4138 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4139 - `OverviewUserInfo()` method 4140 4141 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4142 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4143 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4144 """ 4145 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4146 4147 self.body = str({}) 4148 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4149 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4150 4151 if self.moreDebug: 4152 uLogger.debug("Records with limits of current tariff successfully received") 4153 4154 return rawTariffLimits 4155 4156 def RequestBondCoupons(self, iJSON: dict) -> dict: 4157 """ 4158 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4159 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4160 All dates are in UTC timezone. 4161 4162 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4163 Documentation: 4164 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4165 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4166 4167 See also: `ExtendBondsData()`. 4168 4169 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4170 If raw iJSON is not data of bond then server returns an error [400] with message: 4171 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4172 :return: dictionary with bond payment calendar. Response example 4173 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4174 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4175 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4176 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4177 """ 4178 if iJSON["figi"] is None or not iJSON["figi"]: 4179 uLogger.error("FIGI must be defined for using this method!") 4180 raise Exception("FIGI required") 4181 4182 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4183 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4184 4185 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4186 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4187 self._figi, 4188 startDate, 4189 endDate, 4190 )) 4191 4192 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4193 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4194 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4195 4196 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4197 uLogger.warning("Instrument type is not bond!") 4198 4199 else: 4200 if self.moreDebug: 4201 uLogger.debug("Records about bond payment calendar successfully received") 4202 4203 return calendar 4204 4205 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4206 """ 4207 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4208 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4209 coupon yields, current yields and some statistics etc. 4210 4211 WARNING! This is too long operation if a lot of bonds requested from broker server. 4212 4213 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4214 4215 :param instruments: list of strings with tickers or FIGIs. 4216 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4217 for further used by data scientists or stock analytics. 4218 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4219 In XLSX-file and Pandas DataFrame fields mean: 4220 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4221 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4222 """ 4223 if instruments is None or not instruments: 4224 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4225 raise Exception("Ticker or FIGI required") 4226 4227 if isinstance(instruments, str): 4228 instruments = [instruments] 4229 4230 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4231 4232 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4233 4234 iCount = len(uniqueInstruments) 4235 tooLong = iCount >= 20 4236 if tooLong: 4237 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4238 4239 bonds = None 4240 for i, self._figi in enumerate(uniqueInstruments): 4241 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4242 4243 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4244 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4245 rawBond = self.SearchByFIGI(requestPrice=True) 4246 4247 # Widen raw data with UTC current time (iData["actualDateTime"]): 4248 actualDate = datetime.now(tzutc()) 4249 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4250 4251 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4252 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4253 4254 # Replace some values with human-readable: 4255 iData["nominalCurrency"] = iData["nominal"]["currency"] 4256 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4257 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4258 iData["aciCurrency"] = iData["aciValue"]["currency"] 4259 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4260 iData["issueSize"] = int(iData["issueSize"]) 4261 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4262 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4263 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4264 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4265 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4266 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4267 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4268 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4269 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4270 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4271 4272 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4273 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4274 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4275 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4276 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4277 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4278 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4279 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4280 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4281 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4282 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4283 4284 # Widen raw data with calendar data from `rawCalendar` values: 4285 calendarData = [] 4286 if "events" in iData["rawCalendar"].keys(): 4287 for item in iData["rawCalendar"]["events"]: 4288 calendarData.append({ 4289 "couponDate": item["couponDate"], 4290 "couponNumber": int(item["couponNumber"]), 4291 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4292 "payCurrency": item["payOneBond"]["currency"], 4293 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4294 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4295 "couponStartDate": item["couponStartDate"], 4296 "couponEndDate": item["couponEndDate"], 4297 "couponPeriod": item["couponPeriod"], 4298 }) 4299 4300 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4301 if "maturityDate" not in iData.keys(): 4302 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4303 4304 # Widen raw data with Coupon Rate. 4305 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4306 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4307 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4308 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4309 4310 # Widen raw data with Yield to Maturity (YTM) on current date. 4311 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4312 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4313 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4314 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4315 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4316 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4317 4318 iData["calendar"] = calendarData # adds calendar at the end 4319 4320 # Remove not used data: 4321 iData.pop("uid") 4322 iData.pop("positionUid") 4323 iData.pop("currentPrice") 4324 iData.pop("rawCalendar") 4325 4326 colNames = list(iData.keys()) 4327 if bonds is None: 4328 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4329 4330 else: 4331 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4332 4333 else: 4334 uLogger.warning("Instrument is not a bond!") 4335 4336 processed = round(100 * (i + 1) / iCount, 1) 4337 if tooLong and processed % 5 == 0: 4338 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4339 4340 else: 4341 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4342 4343 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4344 4345 # Saving bonds from Pandas DataFrame to XLSX sheet: 4346 if xlsx and self.bondsXLSXFile: 4347 with pd.ExcelWriter( 4348 path=self.bondsXLSXFile, 4349 date_format=TKS_DATE_FORMAT, 4350 datetime_format=TKS_DATE_TIME_FORMAT, 4351 mode="w", 4352 ) as writer: 4353 bonds.to_excel( 4354 writer, 4355 sheet_name="Extended bonds data", 4356 index=True, 4357 encoding="UTF-8", 4358 freeze_panes=(1, 1), 4359 ) # saving as XLSX-file with freeze first row and column as headers 4360 4361 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4362 4363 return bonds 4364 4365 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4366 """ 4367 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4368 4369 WARNING! This is too long operation if a lot of bonds requested from broker server. 4370 4371 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4372 4373 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4374 extended information about bonds: main info, current prices, bond payment calendar, 4375 coupon yields, current yields and some statistics etc. 4376 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4377 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4378 for further used by data scientists or stock analytics. 4379 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4380 """ 4381 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4382 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4383 4384 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4385 4386 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4387 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4388 calendar = None 4389 for bond in extBonds.iterrows(): 4390 for item in bond[1]["calendar"]: 4391 cData = { 4392 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4393 "couponDate": item["couponDate"], 4394 "figi": bond[1]["figi"], 4395 "ticker": bond[1]["ticker"], 4396 "name": bond[1]["name"], 4397 "couponNumber": item["couponNumber"], 4398 "payOneBond": item["payOneBond"], 4399 "payCurrency": item["payCurrency"], 4400 "couponType": item["couponType"], 4401 "couponPeriod": item["couponPeriod"], 4402 "fixDate": item["fixDate"], 4403 "couponStartDate": item["couponStartDate"], 4404 "couponEndDate": item["couponEndDate"], 4405 } 4406 4407 if calendar is None: 4408 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4409 4410 else: 4411 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4412 4413 if calendar is not None: 4414 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4415 4416 # Saving calendar from Pandas DataFrame to XLSX sheet: 4417 if xlsx: 4418 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4419 4420 with pd.ExcelWriter( 4421 path=xlsxCalendarFile, 4422 date_format=TKS_DATE_FORMAT, 4423 datetime_format=TKS_DATE_TIME_FORMAT, 4424 mode="w", 4425 ) as writer: 4426 humanReadable = calendar.copy(deep=True) 4427 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4428 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4429 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4430 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4431 humanReadable.columns = colNames # human-readable column names 4432 4433 humanReadable.to_excel( 4434 writer, 4435 sheet_name="Bond payments calendar", 4436 index=False, 4437 encoding="UTF-8", 4438 freeze_panes=(1, 2), 4439 ) # saving as XLSX-file with freeze first row and column as headers 4440 4441 del humanReadable # release df in memory 4442 4443 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4444 4445 return calendar 4446 4447 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str: 4448 """ 4449 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4450 Also, creates Markdown file with calendar data, `calendar.md` by default. 4451 4452 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4453 4454 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4455 extended information about bonds: main info, current prices, bond payment calendar, 4456 coupon yields, current yields and some statistics etc. 4457 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4458 :param show: if `True` then also printing bonds payment calendar to the console, 4459 otherwise save to file `calendarFile` only. `False` by default. 4460 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4461 :return: multilines text in Markdown format with bonds payment calendar as a table. 4462 """ 4463 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4464 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles) 4465 4466 infoText = "# Bond payments calendar\n\n" 4467 4468 calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles) # generate Pandas DataFrame with full calendar data 4469 4470 if not (calendar is None or calendar.empty): 4471 splitLine = "| | | | | | | | | |\n" 4472 4473 info = [ 4474 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4475 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4476 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4477 ] 4478 4479 newMonth = False 4480 notOneBond = calendar["figi"].nunique() > 1 4481 for i, bond in enumerate(calendar.iterrows()): 4482 if newMonth and notOneBond: 4483 info.append(splitLine) 4484 4485 info.append( 4486 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4487 " √" if bond[1]["paid"] else " —", 4488 bond[1]["couponDate"].split("T")[0], 4489 bond[1]["figi"], 4490 bond[1]["ticker"], 4491 bond[1]["couponNumber"], 4492 "{} {}".format( 4493 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4494 bond[1]["payCurrency"], 4495 ), 4496 bond[1]["couponType"], 4497 bond[1]["couponPeriod"], 4498 bond[1]["fixDate"].split("T")[0], 4499 ) 4500 ) 4501 4502 if i < len(calendar.values) - 1: 4503 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4504 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4505 newMonth = False if curDate.month == nextDate.month else True 4506 4507 else: 4508 newMonth = False 4509 4510 infoText += "".join(info) 4511 4512 if show and not onlyFiles: 4513 uLogger.info("{}".format(infoText)) 4514 4515 if self.calendarFile is not None and (show or onlyFiles): 4516 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4517 fH.write(infoText) 4518 4519 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4520 4521 if self.useHTMLReports: 4522 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4523 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4524 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4525 4526 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4527 4528 else: 4529 infoText += "No data\n" 4530 4531 return infoText 4532 4533 def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict: 4534 """ 4535 Method for parsing and show simple table with all available user accounts. 4536 4537 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4538 4539 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4540 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4541 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4542 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4543 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4544 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4545 "closed": "—", "access": "Full access" }, ...}}` 4546 """ 4547 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4548 4549 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4550 accounts = { 4551 item["id"]: { 4552 "type": TKS_ACCOUNT_TYPES[item["type"]], 4553 "name": item["name"], 4554 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4555 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4556 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4557 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4558 } for item in rawAccounts["accounts"] 4559 } 4560 4561 # Raw and parsed data with some fields replaced in "stat" section: 4562 view = { 4563 "rawAccounts": rawAccounts, 4564 "stat": accounts, 4565 } 4566 4567 # --- Prepare simple text table with only accounts data in human-readable format: 4568 if show or onlyFiles: 4569 info = [ 4570 "# User accounts\n\n", 4571 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4572 "| Account ID | Type | Status | Name |\n", 4573 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4574 ] 4575 4576 for account in view["stat"].keys(): 4577 info.extend([ 4578 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4579 account, 4580 view["stat"][account]["type"], 4581 view["stat"][account]["status"], 4582 view["stat"][account]["name"], 4583 ) 4584 ]) 4585 4586 infoText = "".join(info) 4587 4588 if show and not onlyFiles: 4589 uLogger.info(infoText) 4590 4591 if self.userAccountsFile and (show or onlyFiles): 4592 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4593 fH.write(infoText) 4594 4595 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4596 4597 if self.useHTMLReports: 4598 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4599 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4600 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4601 4602 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4603 4604 return view 4605 4606 def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict: 4607 """ 4608 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4609 4610 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4611 4612 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4613 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4614 :return: dict with raw parsed data from server and some calculated statistics about it. 4615 """ 4616 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4617 tmpTicker = self._ticker 4618 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4619 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4620 self._ticker = tmpTicker 4621 4622 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4623 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4624 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4625 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4626 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4627 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4628 4629 # This is dict with parsed common user data: 4630 userInfo = { 4631 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4632 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4633 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4634 "tariff": rawUserInfo["tariff"], 4635 } 4636 4637 # This is an array of dict with parsed margin statuses for every account IDs: 4638 margins = {} 4639 for accountId in accounts.keys(): 4640 if rawMargins[accountId]: 4641 margins[accountId] = { 4642 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4643 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4644 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4645 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4646 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4647 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4648 "missing": missing["volume"], 4649 } 4650 4651 else: 4652 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4653 4654 unary = {} # unary-connection limits 4655 for item in rawTariffLimits["unaryLimits"]: 4656 if item["limitPerMinute"] in unary.keys(): 4657 unary[item["limitPerMinute"]].extend(item["methods"]) 4658 4659 else: 4660 unary[item["limitPerMinute"]] = item["methods"] 4661 4662 stream = {} # stream-connection limits 4663 for item in rawTariffLimits["streamLimits"]: 4664 if item["limit"] in stream.keys(): 4665 stream[item["limit"]].extend(item["streams"]) 4666 4667 else: 4668 stream[item["limit"]] = item["streams"] 4669 4670 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4671 limits = { 4672 "unary": unary, 4673 "stream": stream, 4674 } 4675 4676 # Raw and parsed data as an output result: 4677 view = { 4678 "rawUserInfo": rawUserInfo, 4679 "rawAccounts": rawAccounts, 4680 "rawMargins": rawMargins, 4681 "rawTariffLimits": rawTariffLimits, 4682 "stat": { 4683 "overview": overview, 4684 "userInfo": userInfo, 4685 "accounts": accounts, 4686 "margins": margins, 4687 "limits": limits, 4688 }, 4689 } 4690 4691 # --- Prepare text table with user information in human-readable format: 4692 if show or onlyFiles: 4693 info = [ 4694 "# Full user information\n\n", 4695 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4696 "## Common information\n\n", 4697 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4698 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4699 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4700 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4701 "\n## User accounts\n\n", 4702 ] 4703 4704 for account in view["stat"]["accounts"].keys(): 4705 info.extend([ 4706 "### ID: [{}]\n\n".format(account), 4707 "| Parameters | Values |\n", 4708 "|----------------------|--------------------------------------------------------------|\n", 4709 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4710 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4711 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4712 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4713 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4714 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4715 ]) 4716 4717 if margins[account]: 4718 info.extend([ 4719 "| Margin status: | Enabled |\n", 4720 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4721 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4722 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4723 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4724 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4725 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4726 ]) 4727 4728 else: 4729 info.append("| Margin status: | Disabled |\n\n") 4730 4731 info.extend([ 4732 "\n## Current user tariff limits\n", 4733 "\n### See also\n", 4734 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4735 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4736 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4737 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4738 "\n### Unary limits\n", 4739 ]) 4740 4741 if unary: 4742 for key, values in sorted(unary.items()): 4743 info.append("\n* Max requests per minute: {}\n".format(key)) 4744 4745 for value in values: 4746 info.append(" - {}\n".format(value)) 4747 4748 else: 4749 info.append("\nNot available\n") 4750 4751 info.append("\n### Stream limits\n") 4752 4753 if stream: 4754 for key, values in sorted(stream.items()): 4755 info.append("\n* Max stream connections: {}\n".format(key)) 4756 4757 for value in values: 4758 info.append(" - {}\n".format(value)) 4759 4760 else: 4761 info.append("\nNot available\n") 4762 4763 infoText = "".join(info) 4764 4765 if show and not onlyFiles: 4766 uLogger.info(infoText) 4767 4768 if self.userInfoFile and (show or onlyFiles): 4769 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4770 fH.write(infoText) 4771 4772 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4773 4774 if self.useHTMLReports: 4775 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4776 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4777 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4778 4779 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4780 4781 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
88 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 89 """ 90 Main class init. 91 92 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 93 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 94 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 95 :param useCache: use default cache file with raw data to use instead of `iList`. 96 True by default. Cache is auto-update if new day has come. 97 If you don't want to use cache and always updates raw data then set `useCache=False`. 98 :param defaultCache: path to default cache file. `dump.json` by default. 99 """ 100 if token is None or not token: 101 try: 102 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 103 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 104 105 except KeyError: 106 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 107 raise Exception("Token required") 108 109 else: 110 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 111 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 112 113 if accountId is None or not accountId: 114 try: 115 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 116 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 117 118 except KeyError: 119 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 120 121 else: 122 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 123 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 124 125 self.version = __version__ # duplicate here used TKSBrokerAPI main version 126 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 127 128 Latest version: https://pypi.org/project/tksbrokerapi/ 129 """ 130 131 self._tag = "" 132 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 133 134 self.__lock = Lock() # initialize multiprocessing mutex lock 135 136 self._precision = 4 # precision, signs after comma, e.g. 2 for instruments like PLZL, 4 for instruments like USDRUB, if -1 then auto detect it when load data-file 137 138 self.aliases = TKS_TICKER_ALIASES 139 """Some aliases instead official tickers. 140 141 See also: `TKSEnums.TKS_TICKER_ALIASES` 142 """ 143 144 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 145 146 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 147 148 self._ticker = "" 149 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 150 151 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 152 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 153 154 See also: `SearchByTicker()`, `SearchInstruments()`. 155 """ 156 157 self._figi = "" 158 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 159 160 See also: `SearchByFIGI()`, `SearchInstruments()`. 161 """ 162 163 self.depth = 1 164 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 165 166 See also: `GetCurrentPrices()`. 167 """ 168 169 self.server = r"https://invest-public-api.tinkoff.ru/rest" 170 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 171 172 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 173 """ 174 175 uLogger.debug("Broker API server: {}".format(self.server)) 176 177 self.timeout = 15 178 """Server operations timeout in seconds. Default: `15`. 179 180 See also: `SendAPIRequest()`. 181 """ 182 183 self.headers = { 184 "Content-Type": "application/json", 185 "accept": "application/json", 186 "Authorization": "Bearer {}".format(self.token), 187 "x-app-name": "Tim55667757.TKSBrokerAPI", 188 } 189 """ 190 Headers which send in every request to broker server. Please, do not change it! 191 Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`. 192 193 See also: `SendAPIRequest()`. 194 """ 195 196 self.body = None 197 """Request body which send to broker server. Default: `None`. 198 199 See also: `SendAPIRequest()`. 200 """ 201 202 self.moreDebug = False 203 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 204 205 self.useHTMLReports = False 206 """ 207 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 208 209 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 210 """ 211 212 self.historyFile = None 213 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 214 215 See also: `History()`. 216 """ 217 218 self.htmlHistoryFile = "index.html" 219 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 220 221 See also: `ShowHistoryChart()`. 222 """ 223 224 self.instrumentsFile = "instruments.md" 225 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 226 227 See also: `ShowInstrumentsInfo()`. 228 """ 229 230 self.searchResultsFile = "search-results.md" 231 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 232 233 See also: `SearchInstruments()`. 234 """ 235 236 self.pricesFile = "prices.md" 237 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 238 239 See also: `GetListOfPrices()`. 240 """ 241 242 self.infoFile = "info.md" 243 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 244 245 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 246 """ 247 248 self.bondsXLSXFile = "ext-bonds.xlsx" 249 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 250 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 251 252 See also: `ExtendBondsData()`. 253 """ 254 255 self.calendarFile = "calendar.md" 256 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 257 258 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 259 260 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 261 """ 262 263 self.overviewFile = "overview.md" 264 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 265 266 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 267 """ 268 269 self.overviewDigestFile = "overview-digest.md" 270 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 271 272 See also: `Overview()` with parameter `details="digest"`. 273 """ 274 275 self.overviewPositionsFile = "overview-positions.md" 276 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 277 278 See also: `Overview()` with parameter `details="positions"`. 279 """ 280 281 self.overviewOrdersFile = "overview-orders.md" 282 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 283 284 See also: `Overview()` with parameter `details="orders"`. 285 """ 286 287 self.overviewAnalyticsFile = "overview-analytics.md" 288 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 289 290 See also: `Overview()` with parameter `details="analytics"`. 291 """ 292 293 self.overviewBondsCalendarFile = "overview-calendar.md" 294 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 295 296 See also: `Overview()` with parameter `details="calendar"`. 297 """ 298 299 self.reportFile = "deals.md" 300 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 301 302 See also: `Deals()`. 303 """ 304 305 self.withdrawalLimitsFile = "limits.md" 306 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 307 308 See also: `OverviewLimits()` and `RequestLimits()`. 309 """ 310 311 self.userInfoFile = "user-info.md" 312 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 313 314 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 315 """ 316 317 self.userAccountsFile = "accounts.md" 318 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 319 320 See also: `OverviewAccounts()`, `RequestAccounts()`. 321 """ 322 323 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 324 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 325 326 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 327 328 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 329 """ 330 331 self.iList = None # init iList for raw instruments data 332 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 333 334 See also: `Listing()`, `DumpInstruments()`. 335 """ 336 337 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 338 if useCache: 339 if os.path.exists(self.iListDumpFile): 340 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 341 curTime = datetime.now(tzutc()) 342 343 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 344 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 345 346 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 347 348 else: 349 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 350 351 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 352 os.path.abspath(self.iListDumpFile), 353 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 354 )) 355 356 else: 357 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 358 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 359 360 else: 361 self.iList = self.Listing() # request new raw instruments data from broker server 362 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 363 364 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 365 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 366 367 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 368 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it!
Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.
See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.
See also: Overview() with parameter details="calendar".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: "" (empty string).
Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.
Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc.
More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.
See also: SearchByFIGI(), SearchInstruments().
450 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 451 """ 452 Send GET or POST request to broker server and receive JSON object. 453 454 self.header: must be defining with dictionary of headers. 455 self.body: if define then used as request body. None by default. 456 self.timeout: global request timeout, 15 seconds by default. 457 :param url: url with REST request. 458 :param reqType: send "GET" or "POST" request. "GET" by default. 459 :param retry: how many times retry after first request if an 5xx server errors occurred. 460 :param pause: sleep time in seconds between retries. 461 :return: response JSON (dictionary) from broker. 462 """ 463 if reqType.upper() not in ("GET", "POST"): 464 uLogger.error("You can define request type: `GET` or `POST`!") 465 raise Exception("Incorrect value") 466 467 if self.moreDebug: 468 uLogger.debug("Request parameters:") 469 uLogger.debug(" - REST API URL: {}".format(url)) 470 uLogger.debug(" - request type: {}".format(reqType)) 471 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 472 uLogger.debug(" - body:\n{}".format(self.body)) 473 474 # fast hack to avoid all operations with some tickers/FIGI 475 responseJSON = {} 476 oK = True 477 for item in self.exclude: 478 if item in url: 479 if self.moreDebug: 480 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 481 482 oK = False 483 break 484 485 if oK: 486 with self.__lock: # acquire the mutex lock 487 counter = 0 488 response = None 489 errMsg = "" 490 491 while not response and counter <= retry: 492 if reqType == "GET": 493 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 494 495 if reqType == "POST": 496 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 497 498 if self.moreDebug: 499 uLogger.debug("Response:") 500 uLogger.debug(" - status code: {}".format(response.status_code)) 501 uLogger.debug(" - reason: {}".format(response.reason)) 502 uLogger.debug(" - body length: {}".format(len(response.text))) 503 uLogger.debug(" - headers:\n{}".format(response.headers)) 504 505 # Server returns some headers: 506 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 507 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 508 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 509 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 510 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 511 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 512 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 513 sleep(rateLimitWait) 514 515 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 516 if 400 <= response.status_code < 500: 517 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 518 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 519 520 if "code" in response.text and "message" in response.text: 521 msgDict = self._ParseJSON(rawData=response.text) 522 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 523 524 counter = retry + 1 # do not retry for 4xx errors 525 526 if 500 <= response.status_code < 600: 527 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 528 uLogger.debug(" - not oK, {}".format(errMsg)) 529 530 if "code" in response.text and "message" in response.text: 531 errMsgDict = self._ParseJSON(rawData=response.text) 532 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 533 534 counter += 1 535 536 if counter <= retry: 537 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 538 sleep(pause) 539 540 responseJSON = self._ParseJSON(rawData=response.text) 541 542 if errMsg: 543 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 544 uLogger.error(" - not oK, {}".format(errMsg)) 545 546 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
579 def Listing(self) -> dict: 580 """ 581 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 582 583 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 584 """ 585 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 586 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 587 588 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 589 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 590 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 591 592 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 593 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 594 poolUpdater.close() # close the thread pool 595 poolUpdater.join() # wait a moment until all data returns from threads 596 597 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 598 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 599 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 600 601 # calculate minimum price increment (step) for all instruments and set up instrument's type: 602 for iType in iList.keys(): 603 for ticker in iList[iType]: 604 iList[iType][ticker]["type"] = iType 605 606 if "minPriceIncrement" in iList[iType][ticker].keys(): 607 iList[iType][ticker]["step"] = NanoToFloat( 608 iList[iType][ticker]["minPriceIncrement"]["units"], 609 iList[iType][ticker]["minPriceIncrement"]["nano"], 610 ) 611 612 else: 613 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 614 615 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
617 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 618 """ 619 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 620 621 See also: `DumpInstruments()`, `Listing()`. 622 623 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 624 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 625 """ 626 if self.iListDumpFile is None or not self.iListDumpFile: 627 uLogger.error("Output name of dump file must be defined!") 628 raise Exception("Filename required") 629 630 if not self.iList or forceUpdate: 631 self.iList = self.Listing() 632 633 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 634 635 # Save as XLSX with separated sheets for every type of instruments: 636 with pd.ExcelWriter( 637 path=xlsxDumpFile, 638 date_format=TKS_DATE_FORMAT, 639 datetime_format=TKS_DATE_TIME_FORMAT, 640 mode="w", 641 ) as writer: 642 for iType in TKS_INSTRUMENTS: 643 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 644 df = df[sorted(df)] # sorted by column names 645 df = df.applymap( 646 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 647 na_action="ignore", 648 ) # converting numbers from nano-type to float in every cell 649 df.to_excel( 650 writer, 651 sheet_name=iType, 652 encoding="UTF-8", 653 freeze_panes=(1, 1), 654 ) # saving as XLSX-file with freeze first row and column as headers 655 656 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
658 def DumpInstruments(self, forceUpdate: bool = True) -> str: 659 """ 660 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 661 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 662 663 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 664 665 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 666 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 667 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 668 """ 669 if self.iListDumpFile is None or not self.iListDumpFile: 670 uLogger.error("Output name of dump file must be defined!") 671 raise Exception("Filename required") 672 673 if not self.iList or forceUpdate: 674 self.iList = self.Listing() 675 676 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 677 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 678 fH.write(jsonDump) 679 680 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 681 682 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
684 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str: 685 """ 686 Show information about one instrument defined by json data and prints it in Markdown format. 687 688 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 689 690 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 691 :param show: if `True` then also printing information about instrument and its current price. 692 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 693 :return: multilines text in Markdown format with information about one instrument. 694 """ 695 splitLine = "| | |\n" 696 infoText = "" 697 698 if iJSON is not None and iJSON and isinstance(iJSON, dict): 699 info = [ 700 "# Main information\n\n", 701 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 702 "| Parameters | Values |\n", 703 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 704 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 705 "| Full name: | {:<54} |\n".format(iJSON["name"]), 706 ] 707 708 if "sector" in iJSON.keys() and iJSON["sector"]: 709 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 710 711 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 712 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 713 714 info.extend([ 715 splitLine, 716 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 717 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 718 ]) 719 720 if "isin" in iJSON.keys() and iJSON["isin"]: 721 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 722 723 if "classCode" in iJSON.keys(): 724 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 725 726 info.extend([ 727 splitLine, 728 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 729 splitLine, 730 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 731 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 732 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 733 ]) 734 735 if iJSON["figi"]: 736 self._figi = iJSON["figi"] 737 iJSON = iJSON | self.RequestTradingStatus() 738 739 info.extend([ 740 splitLine, 741 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 742 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 743 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 744 ]) 745 746 info.append(splitLine) 747 748 if "type" in iJSON.keys() and iJSON["type"]: 749 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 750 751 if "shareType" in iJSON.keys() and iJSON["shareType"]: 752 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 753 754 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 755 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 756 757 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 758 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 759 760 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 761 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 762 763 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 764 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 765 766 if "focusType" in iJSON.keys() and iJSON["focusType"]: 767 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 768 769 if "assetType" in iJSON.keys() and iJSON["assetType"]: 770 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 771 772 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 773 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 774 775 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 776 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 777 778 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 779 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 780 781 if "currency" in iJSON.keys(): 782 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 783 784 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 785 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 786 787 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 788 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 789 790 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 791 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 792 793 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 794 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 795 796 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 797 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 798 799 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 800 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 801 802 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 803 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 804 805 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 806 info.append("| Perpetual bond: | Yes |\n") 807 808 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 809 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 810 811 iExt = None 812 if iJSON["type"] == "Bonds": 813 info.extend([ 814 splitLine, 815 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 816 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 817 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 818 iJSON["nominal"]["currency"], 819 )), 820 ]) 821 822 if "floatingCouponFlag" in iJSON.keys(): 823 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 824 825 if "amortizationFlag" in iJSON.keys(): 826 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 827 828 info.append(splitLine) 829 830 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 831 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 832 833 if iJSON["figi"]: 834 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 835 836 info.extend([ 837 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 838 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 839 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 840 ]) 841 842 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 843 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 844 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 845 iJSON["aciValue"]["currency"] 846 ))) 847 848 if "currentPrice" in iJSON.keys(): 849 info.append(splitLine) 850 851 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 852 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 853 854 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 855 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 856 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 857 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 858 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 859 860 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 861 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 862 863 info.extend([ 864 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 865 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 866 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 867 )), 868 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 869 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 870 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 871 )), 872 "| Changes between last deal price and last close | {:<54} |\n".format( 873 "{:.2f}%{}".format( 874 iJSON["currentPrice"]["changes"], 875 " ({}{:.2f} {})".format( 876 "+" if bondChangesDelta > 0 else "", 877 bondChangesDelta, 878 aciCurrency 879 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 880 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 881 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 882 currency 883 ), 884 ) 885 ), 886 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 887 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 888 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 889 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 890 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 891 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 892 )), 893 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 894 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 895 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 896 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 897 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 898 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 899 )), 900 ]) 901 902 if "lot" in iJSON.keys(): 903 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 904 905 if "step" in iJSON.keys() and iJSON["step"] != 0: 906 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 907 908 # Add bond payment calendar: 909 if iJSON["type"] == "Bonds": 910 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 911 info.extend(["\n#", strCalendar]) 912 913 infoText += "".join(info) 914 915 if show and not onlyFiles: 916 uLogger.info("{}".format(infoText)) 917 918 if self.infoFile is not None and (show or onlyFiles): 919 with open(self.infoFile, "w", encoding="UTF-8") as fH: 920 fH.write(infoText) 921 922 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 923 924 if self.useHTMLReports: 925 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 926 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 927 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 928 929 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 930 931 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self._ticker] - show: if
Truethen also printing information about instrument and its current price. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multilines text in Markdown format with information about one instrument.
933 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 934 """ 935 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 936 937 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 938 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 939 :return: JSON formatted data with information about instrument. 940 """ 941 tickerJSON = {} 942 if self.moreDebug: 943 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 944 945 if not self._ticker: 946 uLogger.warning("self._ticker variable is not be empty!") 947 948 else: 949 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 950 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 951 raise Exception("Instrument not allowed") 952 953 if not self.iList: 954 self.iList = self.Listing() 955 956 if self._ticker in self.iList["Shares"].keys(): 957 tickerJSON = self.iList["Shares"][self._ticker] 958 if self.moreDebug: 959 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 960 961 elif self._ticker in self.iList["Currencies"].keys(): 962 tickerJSON = self.iList["Currencies"][self._ticker] 963 if self.moreDebug: 964 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 965 966 elif self._ticker in self.iList["Bonds"].keys(): 967 tickerJSON = self.iList["Bonds"][self._ticker] 968 if self.moreDebug: 969 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 970 971 elif self._ticker in self.iList["Etfs"].keys(): 972 tickerJSON = self.iList["Etfs"][self._ticker] 973 if self.moreDebug: 974 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 975 976 elif self._ticker in self.iList["Futures"].keys(): 977 tickerJSON = self.iList["Futures"][self._ticker] 978 if self.moreDebug: 979 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 980 981 if tickerJSON: 982 self._figi = tickerJSON["figi"] 983 984 if requestPrice: 985 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 986 987 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 988 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 989 990 else: 991 tickerJSON["currentPrice"]["changes"] = 0 992 993 if show: 994 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 995 996 else: 997 if show: 998 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 999 1000 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1002 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1003 """ 1004 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1005 1006 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1007 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1008 :return: JSON formatted data with information about instrument. 1009 """ 1010 figiJSON = {} 1011 if self.moreDebug: 1012 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 1013 1014 if not self._figi: 1015 uLogger.warning("self._figi variable is not be empty!") 1016 1017 else: 1018 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1019 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 1020 raise Exception("Instrument not allowed") 1021 1022 if not self.iList: 1023 self.iList = self.Listing() 1024 1025 for item in self.iList["Shares"].keys(): 1026 if self._figi == self.iList["Shares"][item]["figi"]: 1027 figiJSON = self.iList["Shares"][item] 1028 1029 if self.moreDebug: 1030 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1031 1032 break 1033 1034 if not figiJSON: 1035 for item in self.iList["Currencies"].keys(): 1036 if self._figi == self.iList["Currencies"][item]["figi"]: 1037 figiJSON = self.iList["Currencies"][item] 1038 1039 if self.moreDebug: 1040 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1041 1042 break 1043 1044 if not figiJSON: 1045 for item in self.iList["Bonds"].keys(): 1046 if self._figi == self.iList["Bonds"][item]["figi"]: 1047 figiJSON = self.iList["Bonds"][item] 1048 1049 if self.moreDebug: 1050 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1051 1052 break 1053 1054 if not figiJSON: 1055 for item in self.iList["Etfs"].keys(): 1056 if self._figi == self.iList["Etfs"][item]["figi"]: 1057 figiJSON = self.iList["Etfs"][item] 1058 1059 if self.moreDebug: 1060 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1061 1062 break 1063 1064 if not figiJSON: 1065 for item in self.iList["Futures"].keys(): 1066 if self._figi == self.iList["Futures"][item]["figi"]: 1067 figiJSON = self.iList["Futures"][item] 1068 1069 if self.moreDebug: 1070 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1071 1072 break 1073 1074 if figiJSON: 1075 self._figi = figiJSON["figi"] 1076 self._ticker = figiJSON["ticker"] 1077 1078 if requestPrice: 1079 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1080 1081 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1082 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1083 1084 else: 1085 figiJSON["currentPrice"]["changes"] = 0 1086 1087 if show: 1088 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1089 1090 else: 1091 if show: 1092 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1093 1094 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1096 def GetCurrentPrices(self, show: bool = True) -> dict: 1097 """ 1098 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1099 `{"buy": [{"price": 1243.8, "quantity": 193}, 1100 {"price": 1244.0, "quantity": 168}, 1101 {"price": 1244.8, "quantity": 5}, 1102 {"price": 1245.0, "quantity": 61}, 1103 {"price": 1245.4, "quantity": 60}], 1104 "sell": [{"price": 1243.6, "quantity": 8}, 1105 {"price": 1242.6, "quantity": 10}, 1106 {"price": 1242.4, "quantity": 18}, 1107 {"price": 1242.2, "quantity": 50}, 1108 {"price": 1242.0, "quantity": 113}], 1109 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1110 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1111 - sell: list of dicts with Buyers prices, 1112 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1113 - quantity: volume value by current price in lots, 1114 - limitUp: current trade session limit price, maximum, 1115 - limitDown: current trade session limit price, minimum, 1116 - lastPrice: last deal price of the instrument, 1117 - closePrice: previous trade session close price of the instrument. 1118 1119 See also: `SearchByTicker()` and `SearchByFIGI()`. 1120 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1121 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1122 1123 :param show: if `True` then print DOM to log and console. 1124 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1125 If an error occurred then returns an empty record: 1126 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1127 """ 1128 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1129 1130 if self.depth < 1: 1131 uLogger.error("Depth of Market (DOM) must be >=1!") 1132 raise Exception("Incorrect value") 1133 1134 if not (self._ticker or self._figi): 1135 uLogger.error("self._ticker or self._figi variables must be defined!") 1136 raise Exception("Ticker or FIGI required") 1137 1138 if self._ticker and not self._figi: 1139 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1140 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1141 1142 if not self._ticker and self._figi: 1143 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1144 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1145 1146 if not self._figi: 1147 uLogger.error("FIGI is not defined!") 1148 raise Exception("Ticker or FIGI required") 1149 1150 else: 1151 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1152 1153 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1154 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1155 self.body = str({"figi": self._figi, "depth": self.depth}) 1156 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1157 1158 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1159 # list of dicts with sellers orders: 1160 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1161 1162 # list of dicts with buyers orders: 1163 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1164 1165 # max price of instrument at this time: 1166 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1167 1168 # min price of instrument at this time: 1169 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1170 1171 # last price of deal with instrument: 1172 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1173 1174 # last close price of instrument: 1175 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1176 1177 else: 1178 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1179 uLogger.debug("Server response: {}".format(pricesResponse)) 1180 1181 if show: 1182 if prices["buy"] or prices["sell"]: 1183 info = [ 1184 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1185 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1186 self._ticker, 1187 self._figi, 1188 self.depth, 1189 ), 1190 "-" * 60, "\n", 1191 " Orders of Buyers | Orders of Sellers\n", 1192 "-" * 60, "\n", 1193 " Sell prices (volumes) | Buy prices (volumes)\n", 1194 "-" * 60, "\n", 1195 ] 1196 1197 if not prices["buy"]: 1198 info.append(" | No orders!\n") 1199 sumBuy = 0 1200 1201 else: 1202 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1203 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1204 for item in maxMinSorted: 1205 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1206 1207 if not prices["sell"]: 1208 info.append("No orders! |\n") 1209 sumSell = 0 1210 1211 else: 1212 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1213 for item in prices["sell"]: 1214 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1215 1216 info.extend([ 1217 "-" * 60, "\n", 1218 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1219 "-" * 60, "\n", 1220 ]) 1221 1222 infoText = "".join(info) 1223 1224 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1225 1226 else: 1227 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1228 1229 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1231 def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str: 1232 """ 1233 This method get and show information about all available broker instruments for current user account. 1234 If `instrumentsFile` string is not empty then also save information to this file. 1235 1236 :param show: if `True` then print results to console, if `False` — print only to file. 1237 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1238 :return: multi-lines string with all available broker instruments. 1239 """ 1240 if not self.iList: 1241 self.iList = self.Listing() 1242 1243 info = [ 1244 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1245 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1246 ] 1247 1248 # add instruments count by type: 1249 for iType in self.iList.keys(): 1250 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1251 1252 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1253 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1254 1255 # generating info tables with all instruments by type: 1256 for iType in self.iList.keys(): 1257 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1258 1259 for instrument in self.iList[iType].keys(): 1260 iName = self.iList[iType][instrument]["name"] # instrument's name 1261 if len(iName) > 57: 1262 iName = "{}...".format(iName[:54]) # right trim for a long string 1263 1264 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1265 self.iList[iType][instrument]["ticker"], 1266 iName, 1267 self.iList[iType][instrument]["figi"], 1268 self.iList[iType][instrument]["currency"], 1269 self.iList[iType][instrument]["lot"], 1270 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1271 )) 1272 1273 infoText = "".join(info) 1274 1275 if show and not onlyFiles: 1276 uLogger.info(infoText) 1277 1278 if self.instrumentsFile and (show or onlyFiles): 1279 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1280 fH.write(infoText) 1281 1282 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1283 1284 if self.useHTMLReports: 1285 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1286 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1287 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1288 1289 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1290 1291 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse— print only to file. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multi-lines string with all available broker instruments.
1293 def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict: 1294 """ 1295 This method search and show information about instruments by part of its ticker, FIGI or name. 1296 If `searchResultsFile` string is not empty then also save information to this file. 1297 1298 :param pattern: string with part of ticker, FIGI or instrument's name. 1299 :param show: if `True` then print results to console, if `False` — return list of result only. 1300 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1301 :return: list of dictionaries with all found instruments. 1302 """ 1303 if not self.iList: 1304 self.iList = self.Listing() 1305 1306 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1307 compiledPattern = re.compile(pattern, re.IGNORECASE) 1308 1309 for iType in self.iList: 1310 for instrument in self.iList[iType].values(): 1311 searchResult = compiledPattern.search(" ".join( 1312 [instrument["ticker"], instrument["figi"], instrument["name"]] 1313 )) 1314 1315 if searchResult: 1316 searchResults[iType][instrument["ticker"]] = instrument 1317 1318 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1319 info = [ 1320 "# Search results\n\n", 1321 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1322 "* **Search pattern:** [{}]\n".format(pattern), 1323 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1324 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1325 ] 1326 infoShort = info[:] 1327 1328 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1329 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1330 skippedLine = "| ... | ... | ... | ... |\n" 1331 1332 if resultsLen == 0: 1333 info.append("\nNo results\n") 1334 infoShort.append("\nNo results\n") 1335 uLogger.warning("No results. Try changing your search pattern.") 1336 1337 else: 1338 for iType in searchResults: 1339 iTypeValuesCount = len(searchResults[iType].values()) 1340 if iTypeValuesCount > 0: 1341 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1342 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1343 1344 for instrument in searchResults[iType].values(): 1345 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1346 instrument["type"], 1347 instrument["ticker"], 1348 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1349 instrument["figi"], 1350 )) 1351 1352 if iTypeValuesCount <= 5: 1353 infoShort.extend(info[-iTypeValuesCount:]) 1354 1355 else: 1356 infoShort.extend(info[-5:]) 1357 infoShort.append(skippedLine) 1358 1359 infoText = "".join(info) 1360 infoTextShort = "".join(infoShort) 1361 1362 if show and not onlyFiles: 1363 uLogger.info(infoTextShort) 1364 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1365 1366 if self.searchResultsFile and (show or onlyFiles): 1367 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1368 fH.write(infoText) 1369 1370 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1371 1372 if self.useHTMLReports: 1373 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1374 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1375 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1376 1377 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1378 1379 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse— return list of result only. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
list of dictionaries with all found instruments.
1381 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1382 """ 1383 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1384 1385 :param instruments: list of strings with tickers or FIGIs. 1386 :return: list with unique instrument FIGIs only. 1387 """ 1388 requestedInstruments = [] 1389 for iName in instruments: 1390 if iName not in self.aliases.keys(): 1391 if iName not in requestedInstruments: 1392 requestedInstruments.append(iName) 1393 1394 else: 1395 if iName not in requestedInstruments: 1396 if self.aliases[iName] not in requestedInstruments: 1397 requestedInstruments.append(self.aliases[iName]) 1398 1399 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1400 1401 onlyUniqueFIGIs = [] 1402 for iName in requestedInstruments: 1403 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1404 continue 1405 1406 self._ticker = iName 1407 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1408 1409 if not iData: 1410 self._ticker = "" 1411 self._figi = iName 1412 1413 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1414 1415 if not iData: 1416 self._figi = "" 1417 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1418 1419 if iData and iData["figi"] not in onlyUniqueFIGIs: 1420 onlyUniqueFIGIs.append(iData["figi"]) 1421 1422 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1423 1424 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1426 def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]: 1427 """ 1428 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1429 1430 See limits: https://tinkoff.github.io/investAPI/limits/ 1431 1432 If `pricesFile` string is not empty then also save information to this file. 1433 1434 :param instruments: list of strings with tickers or FIGIs. 1435 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1436 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1437 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1438 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1439 """ 1440 if instruments is None or not instruments: 1441 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1442 raise Exception("Ticker or FIGI required") 1443 1444 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1445 1446 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1447 1448 iList = [] # trying to get info and current prices about all unique instruments: 1449 for self._figi in onlyUniqueFIGIs: 1450 iData = self.SearchByFIGI(requestPrice=True, show=False) 1451 iList.append(iData) 1452 1453 self.ShowListOfPrices(iList, show, onlyFiles) 1454 1455 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1457 def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str: 1458 """ 1459 Show table contains current prices of given instruments. 1460 1461 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1462 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1463 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1464 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1465 :return: multilines text in Markdown format as a table contains current prices. 1466 """ 1467 infoText = "" 1468 1469 if show or self.pricesFile or onlyFiles: 1470 info = [ 1471 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1472 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1473 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1474 ] 1475 1476 for item in iList: 1477 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1478 item["ticker"], 1479 item["figi"], 1480 item["type"], 1481 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1482 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1483 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1484 "{} / {}".format( 1485 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1486 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1487 ), 1488 "{} / {}".format( 1489 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1490 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1491 ), 1492 item["currency"], 1493 )) 1494 1495 infoText = "".join(info) 1496 1497 if show and not onlyFiles: 1498 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1499 1500 if self.pricesFile and (show or onlyFiles): 1501 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1502 fH.write(infoText) 1503 1504 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1505 1506 if self.useHTMLReports: 1507 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1508 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1509 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1510 1511 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1512 1513 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multilines text in Markdown format as a table contains current prices.
1515 def RequestTradingStatus(self) -> dict: 1516 """ 1517 Requesting trading status for the instrument defined by `figi` variable. 1518 1519 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1520 1521 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1522 1523 :return: dictionary with trading status attributes. Response example: 1524 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1525 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1526 """ 1527 if self._figi is None or not self._figi: 1528 uLogger.error("Variable `figi` must be defined for using this method!") 1529 raise Exception("FIGI required") 1530 1531 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1532 1533 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1534 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1535 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1536 1537 if self.moreDebug: 1538 uLogger.debug("Records about current trading status successfully received") 1539 1540 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1542 def RequestPortfolio(self) -> dict: 1543 """ 1544 Requesting actual user's portfolio for current `accountId`. 1545 1546 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1547 1548 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1549 1550 :return: dictionary with user's portfolio. 1551 """ 1552 if self.accountId is None or not self.accountId: 1553 uLogger.error("Variable `accountId` must be defined for using this method!") 1554 raise Exception("Account ID required") 1555 1556 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1557 1558 self.body = str({"accountId": self.accountId}) 1559 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1560 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1561 1562 if self.moreDebug: 1563 uLogger.debug("Records about user's portfolio successfully received") 1564 1565 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1567 def RequestPositions(self) -> dict: 1568 """ 1569 Requesting open positions by currencies and instruments for current `accountId`. 1570 1571 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1572 1573 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1574 1575 :return: dictionary with open positions by instruments. 1576 """ 1577 if self.accountId is None or not self.accountId: 1578 uLogger.error("Variable `accountId` must be defined for using this method!") 1579 raise Exception("Account ID required") 1580 1581 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1582 1583 self.body = str({"accountId": self.accountId}) 1584 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1585 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1586 1587 if self.moreDebug: 1588 uLogger.debug("Records about current open positions successfully received") 1589 1590 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1592 def RequestPendingOrders(self) -> list: 1593 """ 1594 Requesting current actual pending limit orders for current `accountId`. 1595 1596 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1597 1598 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1599 1600 :return: list of dictionaries with pending limit orders. 1601 """ 1602 if self.accountId is None or not self.accountId: 1603 uLogger.error("Variable `accountId` must be defined for using this method!") 1604 raise Exception("Account ID required") 1605 1606 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1607 1608 self.body = str({"accountId": self.accountId}) 1609 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1610 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1611 1612 if "orders" in rawResponse.keys(): 1613 rawOrders = rawResponse["orders"] 1614 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1615 1616 else: 1617 rawOrders = [] 1618 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1619 1620 return rawOrders
Requesting current actual pending limit orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending limit orders.
1622 def RequestStopOrders(self) -> list: 1623 """ 1624 Requesting current actual stop orders for current `accountId`. 1625 1626 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1627 1628 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1629 1630 :return: list of dictionaries with stop orders. 1631 """ 1632 if self.accountId is None or not self.accountId: 1633 uLogger.error("Variable `accountId` must be defined for using this method!") 1634 raise Exception("Account ID required") 1635 1636 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1637 1638 self.body = str({"accountId": self.accountId}) 1639 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1640 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1641 1642 if "stopOrders" in rawResponse.keys(): 1643 rawStopOrders = rawResponse["stopOrders"] 1644 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1645 1646 else: 1647 rawStopOrders = [] 1648 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1649 1650 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1652 def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict: 1653 """ 1654 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1655 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1656 and `overviewBondsCalendarFile` are defined then also save information to file. 1657 1658 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1659 many requests about the state of the portfolio, and then, based on the received data, a large number 1660 of calculation and statistics are collected. 1661 1662 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1663 :param details: how detailed should the information be? 1664 - `full` — shows full available information about portfolio status (by default), 1665 - `positions` — shows only open positions, 1666 - `orders` — shows only sections of open limits and stop orders. 1667 - `digest` — show a short digest of the portfolio status, 1668 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1669 - `calendar` — shows only the bonds calendar section (if these present in portfolio). 1670 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1671 :return: dictionary with client's raw portfolio and some statistics. 1672 """ 1673 if self.accountId is None or not self.accountId: 1674 uLogger.error("Variable `accountId` must be defined for using this method!") 1675 raise Exception("Account ID required") 1676 1677 view = { 1678 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1679 "headers": {}, # list of dictionaries, response headers without "positions" section 1680 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1681 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1682 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1683 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1684 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1685 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1686 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1687 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1688 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1689 }, 1690 "stat": { # --- some statistics calculated using "raw" sections: 1691 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1692 "availableRUB": 0., # available rubles (without other currencies) 1693 "blockedRUB": 0., # blocked sum in Russian Rouble 1694 "totalChangesRUB": 0., # changes for all open trades in RUB 1695 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1696 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1697 "sharesCostRUB": 0., # costs of all shares in RUB 1698 "bondsCostRUB": 0., # costs of all bonds in RUB 1699 "etfsCostRUB": 0., # costs of all etfs in RUB 1700 "futuresCostRUB": 0., # costs of all futures in RUB 1701 "Currencies": [], # list of dictionaries of all currencies statistics 1702 "Shares": [], # list of dictionaries of all shares statistics 1703 "Bonds": [], # list of dictionaries of all bonds statistics 1704 "Etfs": [], # list of dictionaries of all etfs statistics 1705 "Futures": [], # list of dictionaries of all futures statistics 1706 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1707 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1708 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1709 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1710 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1711 }, 1712 "analytics": { # --- some analytics of portfolio: 1713 "distrByAssets": {}, # portfolio distribution by assets 1714 "distrByCompanies": {}, # portfolio distribution by companies 1715 "distrBySectors": {}, # portfolio distribution by sectors 1716 "distrByCurrencies": {}, # portfolio distribution by currencies 1717 "distrByCountries": {}, # portfolio distribution by countries 1718 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1719 } 1720 } 1721 1722 details = details.lower() 1723 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1724 if details not in availableDetails: 1725 details = "full" 1726 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1727 1728 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1729 1730 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1731 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1732 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1733 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1734 1735 # save response headers without "positions" section: 1736 for key in portfolioResponse.keys(): 1737 if key != "positions": 1738 view["raw"]["headers"][key] = portfolioResponse[key] 1739 1740 else: 1741 continue 1742 1743 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1744 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1745 for item in portfolioResponse["positions"]: 1746 if item["instrumentType"] == "currency": 1747 self._figi = item["figi"] 1748 if not self._figi and item["ticker"]: 1749 self._ticker = item["ticker"] 1750 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1751 1752 curr = self.SearchByFIGI(requestPrice=False) 1753 1754 # current price of currency in RUB: 1755 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1756 "name": curr["name"], 1757 "currentPrice": NanoToFloat( 1758 item["currentPrice"]["units"], 1759 item["currentPrice"]["nano"] 1760 ), 1761 } 1762 1763 view["raw"]["Currencies"].append(item) 1764 1765 elif item["instrumentType"] == "share": 1766 view["raw"]["Shares"].append(item) 1767 1768 elif item["instrumentType"] == "bond": 1769 view["raw"]["Bonds"].append(item) 1770 1771 elif item["instrumentType"] == "etf": 1772 view["raw"]["Etfs"].append(item) 1773 1774 elif item["instrumentType"] == "futures": 1775 view["raw"]["Futures"].append(item) 1776 1777 else: 1778 continue 1779 1780 # how many volume of currencies (by ISO currency name) are blocked: 1781 for item in view["raw"]["positions"]["blocked"]: 1782 blocked = NanoToFloat(item["units"], item["nano"]) 1783 if blocked > 0: 1784 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1785 1786 # how many volume of instruments (by FIGI) are blocked: 1787 for item in view["raw"]["positions"]["securities"]: 1788 blocked = int(item["blocked"]) 1789 if blocked > 0: 1790 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1791 1792 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1793 1794 if "rub" in allBlocked.keys(): 1795 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1796 1797 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1798 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1799 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1800 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1801 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1802 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1803 view["stat"]["portfolioCostRUB"] = sum([ 1804 view["stat"]["allCurrenciesCostRUB"], 1805 view["stat"]["sharesCostRUB"], 1806 view["stat"]["bondsCostRUB"], 1807 view["stat"]["etfsCostRUB"], 1808 view["stat"]["futuresCostRUB"], 1809 ]) 1810 1811 # --- calculating some portfolio statistics: 1812 byComp = {} # distribution by companies 1813 bySect = {} # distribution by sectors 1814 byCurr = {} # distribution by currencies (include RUB) 1815 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1816 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1817 1818 for item in portfolioResponse["positions"]: 1819 self._figi = item["figi"] 1820 if not self._figi and item["ticker"]: 1821 self._ticker = item["ticker"] 1822 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1823 1824 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1825 1826 if instrument: 1827 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1828 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1829 1830 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1831 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1832 1833 else: 1834 blocked = 0 1835 1836 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1837 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1838 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1839 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1840 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1841 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1842 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1843 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1844 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1845 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1846 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1847 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1848 1849 statData = { 1850 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1851 "ticker": instrument["ticker"], # ticker by FIGI 1852 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1853 "volume": volume, # available volume of instrument 1854 "lots": lots, # volume in lots of instrument 1855 "direction": direction, # direction of an instrument's position: short or long 1856 "blocked": blocked, # blocked volume of currency or instrument 1857 "currentPrice": curPrice, # current instrument's price in basic asset 1858 "average": average, # current average position price 1859 "cost": cost, # current cost of all volume of instrument in basic asset 1860 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1861 "costRUB": costRUB, # cost of instrument in ruble 1862 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1863 "profit": profit, # expected profit at current moment 1864 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1865 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1866 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1867 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1868 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1869 "step": instrument["step"], # minimum price increment 1870 } 1871 1872 # adding distribution by unique countries: 1873 if statData["country"] not in byCountry.keys(): 1874 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1875 1876 else: 1877 byCountry[statData["country"]]["cost"] += costRUB 1878 byCountry[statData["country"]]["percent"] += percentCostRUB 1879 1880 if item["instrumentType"] != "currency": 1881 # adding distribution by unique companies: 1882 if statData["name"]: 1883 if statData["name"] not in byComp.keys(): 1884 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1885 1886 else: 1887 byComp[statData["name"]]["cost"] += costRUB 1888 byComp[statData["name"]]["percent"] += percentCostRUB 1889 1890 # adding distribution by unique sectors: 1891 if statData["sector"] not in bySect.keys(): 1892 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1893 1894 else: 1895 bySect[statData["sector"]]["cost"] += costRUB 1896 bySect[statData["sector"]]["percent"] += percentCostRUB 1897 1898 # adding distribution by unique currencies: 1899 if currency not in byCurr.keys(): 1900 byCurr[currency] = { 1901 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1902 "cost": costRUB, 1903 "percent": percentCostRUB 1904 } 1905 1906 else: 1907 byCurr[currency]["cost"] += costRUB 1908 byCurr[currency]["percent"] += percentCostRUB 1909 1910 # saving statistics for every instrument: 1911 if item["instrumentType"] == "currency": 1912 view["stat"]["Currencies"].append(statData) 1913 1914 # update dict with free funds for trading (total - blocked) by currencies 1915 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1916 view["stat"]["funds"][currency] = { 1917 "total": volume, 1918 "totalCostRUB": costRUB, # total volume cost in rubles 1919 "free": volume - blocked, 1920 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1921 } 1922 1923 elif item["instrumentType"] == "share": 1924 view["stat"]["Shares"].append(statData) 1925 1926 elif item["instrumentType"] == "bond": 1927 view["stat"]["Bonds"].append(statData) 1928 1929 elif item["instrumentType"] == "etf": 1930 view["stat"]["Etfs"].append(statData) 1931 1932 elif item["instrumentType"] == "Futures": 1933 view["stat"]["Futures"].append(statData) 1934 1935 else: 1936 continue 1937 1938 # total changes in Russian Ruble: 1939 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1940 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1941 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1942 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1943 view["stat"]["funds"]["rub"] = { 1944 "total": view["stat"]["availableRUB"], 1945 "totalCostRUB": view["stat"]["availableRUB"], 1946 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1947 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1948 } 1949 1950 # --- pending limit orders sector data: 1951 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1952 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1953 1954 for item in view["raw"]["orders"]: 1955 self._figi = item["figi"] 1956 1957 if item["figi"] not in uniquePendingOrdersFIGIs: 1958 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1959 1960 uniquePendingOrdersFIGIs.append(item["figi"]) 1961 uniquePendingOrders[item["figi"]] = instrument 1962 1963 else: 1964 instrument = uniquePendingOrders[item["figi"]] 1965 1966 if instrument: 1967 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1968 orderType = TKS_ORDER_TYPES[item["orderType"]] 1969 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1970 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1971 1972 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1973 if item["direction"] == "ORDER_DIRECTION_BUY": 1974 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1975 1976 else: 1977 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1978 1979 # requested price for order execution: 1980 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1981 1982 # necessary changes in percent to reach target from current price: 1983 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1984 1985 view["stat"]["orders"].append({ 1986 "orderID": item["orderId"], # orderId number parameter of current order 1987 "figi": item["figi"], # FIGI identification 1988 "ticker": instrument["ticker"], # ticker name by FIGI 1989 "lotsRequested": item["lotsRequested"], # requested lots value 1990 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1991 "currentPrice": lastPrice, # current instrument's price for defined action 1992 "targetPrice": target, # requested price for order execution in base currency 1993 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1994 "percentChanges": changes, # changes in percent to target from current price 1995 "currency": item["currency"], # instrument's currency name 1996 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1997 "type": orderType, # type of order from TKS_ORDER_TYPES 1998 "status": orderState, # order status from TKS_ORDER_STATES 1999 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 2000 }) 2001 2002 # --- stop orders sector data: 2003 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 2004 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 2005 2006 for item in view["raw"]["stopOrders"]: 2007 self._figi = item["figi"] 2008 2009 if item["figi"] not in uniqueStopOrdersFIGIs: 2010 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 2011 2012 uniqueStopOrdersFIGIs.append(item["figi"]) 2013 uniqueStopOrders[item["figi"]] = instrument 2014 2015 else: 2016 instrument = uniqueStopOrders[item["figi"]] 2017 2018 if instrument: 2019 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 2020 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 2021 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 2022 2023 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 2024 if "expirationTime" in item.keys(): 2025 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 2026 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 2027 2028 else: 2029 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 2030 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 2031 2032 # current instrument's price (last sellers order if buy, and last buyers order if sell): 2033 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 2034 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2035 2036 else: 2037 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2038 2039 # requested price when stop-order executed: 2040 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2041 2042 # price for limit-order, set up when stop-order executed: 2043 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2044 2045 # necessary changes in percent to reach target from current price: 2046 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2047 2048 view["stat"]["stopOrders"].append({ 2049 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2050 "figi": item["figi"], # FIGI identification 2051 "ticker": instrument["ticker"], # ticker name by FIGI 2052 "lotsRequested": item["lotsRequested"], # requested lots value 2053 "currentPrice": lastPrice, # current instrument's price for defined action 2054 "targetPrice": target, # requested price for stop-order execution in base currency 2055 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2056 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2057 "percentChanges": changes, # changes in percent to target from current price 2058 "currency": item["currency"], # instrument's currency name 2059 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2060 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2061 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2062 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2063 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2064 }) 2065 2066 # --- calculating data for analytics section: 2067 # portfolio distribution by assets: 2068 view["analytics"]["distrByAssets"] = { 2069 "Ruble": { 2070 "uniques": 1, 2071 "cost": view["stat"]["availableRUB"], 2072 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2073 }, 2074 "Currencies": { 2075 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2076 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2077 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2078 }, 2079 "Shares": { 2080 "uniques": len(view["stat"]["Shares"]), 2081 "cost": view["stat"]["sharesCostRUB"], 2082 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2083 }, 2084 "Bonds": { 2085 "uniques": len(view["stat"]["Bonds"]), 2086 "cost": view["stat"]["bondsCostRUB"], 2087 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2088 }, 2089 "Etfs": { 2090 "uniques": len(view["stat"]["Etfs"]), 2091 "cost": view["stat"]["etfsCostRUB"], 2092 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2093 }, 2094 "Futures": { 2095 "uniques": len(view["stat"]["Futures"]), 2096 "cost": view["stat"]["futuresCostRUB"], 2097 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2098 }, 2099 } 2100 2101 # portfolio distribution by companies: 2102 view["analytics"]["distrByCompanies"]["All money cash"] = { 2103 "ticker": "", 2104 "cost": view["stat"]["allCurrenciesCostRUB"], 2105 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2106 } 2107 view["analytics"]["distrByCompanies"].update(byComp) 2108 2109 # portfolio distribution by sectors: 2110 view["analytics"]["distrBySectors"]["All money cash"] = { 2111 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2112 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2113 } 2114 view["analytics"]["distrBySectors"].update(bySect) 2115 2116 # portfolio distribution by currencies: 2117 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2118 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2119 2120 if self.moreDebug: 2121 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2122 2123 view["analytics"]["distrByCurrencies"].update(byCurr) 2124 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2125 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2126 2127 # portfolio distribution by countries: 2128 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2129 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2130 2131 if self.moreDebug: 2132 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2133 2134 view["analytics"]["distrByCountries"].update(byCountry) 2135 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2136 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2137 2138 # --- Prepare text statistics overview in human-readable: 2139 if show or onlyFiles: 2140 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2141 2142 # Whatever the value `details`, header not changes: 2143 info = [ 2144 "# Client's portfolio\n\n", 2145 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2146 "* **Account ID:** [{}]\n".format(self.accountId), 2147 ] 2148 2149 if details in ["full", "positions", "digest"]: 2150 info.extend([ 2151 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2152 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2153 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2154 view["stat"]["totalChangesRUB"], 2155 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2156 view["stat"]["totalChangesPercentRUB"], 2157 ), 2158 ]) 2159 2160 if details in ["full", "positions"]: 2161 info.extend([ 2162 "## Open positions\n\n", 2163 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2164 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2165 "| **Ruble:** | {:>31} | | | | | |\n".format( 2166 "{:.2f} ({:.2f}) rub".format( 2167 view["stat"]["availableRUB"], 2168 view["stat"]["blockedRUB"], 2169 ) 2170 ) 2171 ]) 2172 2173 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2174 return [ 2175 "| | | | | | | |\n", 2176 "| {:<27} | | | | | {:>19} | |\n".format( 2177 noTradeStr if noTradeStr else typeStr, 2178 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2179 ), 2180 ] 2181 2182 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2183 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2184 "{} [{}]".format(data["ticker"], data["figi"]), 2185 "{:.2f} ({:.2f}) {}".format( 2186 data["volume"], 2187 data["blocked"], 2188 data["currency"], 2189 ) if isCurr else "{:.0f} ({:.0f})".format( 2190 data["volume"], 2191 data["blocked"], 2192 ), 2193 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2194 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2195 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2196 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2197 "{}{:.2f} {} ({}{:.2f}%)".format( 2198 "+" if data["profit"] > 0 else "", 2199 data["profit"], data["baseCurrencyName"], 2200 "+" if data["percentProfit"] > 0 else "", 2201 data["percentProfit"], 2202 ), 2203 ) 2204 2205 # --- Show currencies section: 2206 if view["stat"]["Currencies"]: 2207 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2208 for item in view["stat"]["Currencies"]: 2209 info.append(_InfoStr(item, isCurr=True)) 2210 2211 else: 2212 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2213 2214 # --- Show shares section: 2215 if view["stat"]["Shares"]: 2216 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2217 2218 for item in view["stat"]["Shares"]: 2219 info.append(_InfoStr(item)) 2220 2221 else: 2222 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2223 2224 # --- Show bonds section: 2225 if view["stat"]["Bonds"]: 2226 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2227 2228 for item in view["stat"]["Bonds"]: 2229 info.append(_InfoStr(item)) 2230 2231 else: 2232 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2233 2234 # --- Show etfs section: 2235 if view["stat"]["Etfs"]: 2236 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2237 2238 for item in view["stat"]["Etfs"]: 2239 info.append(_InfoStr(item)) 2240 2241 else: 2242 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2243 2244 # --- Show futures section: 2245 if view["stat"]["Futures"]: 2246 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2247 2248 for item in view["stat"]["Futures"]: 2249 info.append(_InfoStr(item)) 2250 2251 else: 2252 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2253 2254 if details in ["full", "orders"]: 2255 # --- Show pending limit orders section: 2256 if view["stat"]["orders"]: 2257 info.extend([ 2258 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2259 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2260 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2261 ]) 2262 2263 for item in view["stat"]["orders"]: 2264 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2265 "{} [{}]".format(item["ticker"], item["figi"]), 2266 item["orderID"], 2267 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2268 "{} {} ({}{:.2f}%)".format( 2269 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2270 item["baseCurrencyName"], 2271 "+" if item["percentChanges"] > 0 else "", 2272 float(item["percentChanges"]), 2273 ), 2274 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2275 item["action"], 2276 item["type"], 2277 item["date"], 2278 )) 2279 2280 else: 2281 info.append("\n## Total pending limit-orders: [0]\n") 2282 2283 # --- Show stop orders section: 2284 if view["stat"]["stopOrders"]: 2285 info.extend([ 2286 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2287 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2288 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2289 ]) 2290 2291 for item in view["stat"]["stopOrders"]: 2292 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2293 "{} [{}]".format(item["ticker"], item["figi"]), 2294 item["orderID"], 2295 item["lotsRequested"], 2296 "{} {} ({}{:.2f}%)".format( 2297 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2298 item["baseCurrencyName"], 2299 "+" if item["percentChanges"] > 0 else "", 2300 float(item["percentChanges"]), 2301 ), 2302 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2303 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2304 item["action"], 2305 item["type"], 2306 item["expType"], 2307 item["createDate"], 2308 item["expDate"], 2309 )) 2310 2311 else: 2312 info.append("\n## Total stop-orders: [0]\n") 2313 2314 if details in ["full", "analytics"]: 2315 # -- Show analytics section: 2316 if view["stat"]["portfolioCostRUB"] > 0: 2317 info.extend([ 2318 "\n# Analytics\n\n" 2319 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2320 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2321 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2322 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2323 view["stat"]["totalChangesRUB"], 2324 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2325 view["stat"]["totalChangesPercentRUB"], 2326 ), 2327 "\n## Portfolio distribution by assets\n" 2328 "\n| Type | Uniques | Percent | Current cost |\n", 2329 "|------------------------------------|---------|---------|--------------------|\n", 2330 ]) 2331 2332 for key in view["analytics"]["distrByAssets"].keys(): 2333 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2334 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2335 key, 2336 view["analytics"]["distrByAssets"][key]["uniques"], 2337 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2338 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2339 )) 2340 2341 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2342 2343 info.extend([ 2344 "\n## Portfolio distribution by companies\n" 2345 "\n| Company | Percent | Current cost |\n", 2346 aSepLine, 2347 ]) 2348 2349 for company in view["analytics"]["distrByCompanies"].keys(): 2350 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2351 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2352 "{}{}".format( 2353 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2354 company, 2355 ), 2356 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2357 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2358 )) 2359 2360 info.extend([ 2361 "\n## Portfolio distribution by sectors\n" 2362 "\n| Sector | Percent | Current cost |\n", 2363 aSepLine, 2364 ]) 2365 2366 for sector in view["analytics"]["distrBySectors"].keys(): 2367 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2368 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2369 sector, 2370 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2371 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2372 )) 2373 2374 info.extend([ 2375 "\n## Portfolio distribution by currencies\n" 2376 "\n| Instruments currencies | Percent | Current cost |\n", 2377 aSepLine, 2378 ]) 2379 2380 for curr in view["analytics"]["distrByCurrencies"].keys(): 2381 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2382 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2383 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2384 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2385 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2386 )) 2387 2388 info.extend([ 2389 "\n## Portfolio distribution by countries\n" 2390 "\n| Assets by country | Percent | Current cost |\n", 2391 aSepLine, 2392 ]) 2393 2394 for country in view["analytics"]["distrByCountries"].keys(): 2395 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2396 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2397 country, 2398 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2399 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2400 )) 2401 2402 if details in ["full", "calendar"]: 2403 # -- Show bonds payment calendar section: 2404 if view["stat"]["Bonds"]: 2405 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2406 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2407 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2408 2409 else: 2410 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2411 2412 infoText = "".join(info) 2413 2414 if show and not onlyFiles: 2415 uLogger.info(infoText) 2416 2417 if details == "full" and self.overviewFile: 2418 filename = self.overviewFile 2419 2420 elif details == "digest" and self.overviewDigestFile: 2421 filename = self.overviewDigestFile 2422 2423 elif details == "positions" and self.overviewPositionsFile: 2424 filename = self.overviewPositionsFile 2425 2426 elif details == "orders" and self.overviewOrdersFile: 2427 filename = self.overviewOrdersFile 2428 2429 elif details == "analytics" and self.overviewAnalyticsFile: 2430 filename = self.overviewAnalyticsFile 2431 2432 elif details == "calendar" and self.overviewBondsCalendarFile: 2433 filename = self.overviewBondsCalendarFile 2434 2435 else: 2436 filename = "" 2437 2438 if filename and (show or onlyFiles): 2439 with open(filename, "w", encoding="UTF-8") as fH: 2440 fH.write(infoText) 2441 2442 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2443 2444 if self.useHTMLReports: 2445 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2446 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2447 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2448 2449 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2450 2451 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
and overviewBondsCalendarFile are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be?
full— shows full available information about portfolio status (by default),positions— shows only open positions,orders— shows only sections of open limits and stop orders.digest— show a short digest of the portfolio status,analytics— shows only the analytics section and the distribution of the portfolio by various categories,calendar— shows only the bonds calendar section (if these present in portfolio).
- onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dictionary with client's raw portfolio and some statistics.
2453 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]: 2454 """ 2455 Returns history operations between two given dates for current `accountId`. 2456 If `reportFile` string is not empty then also save human-readable report. 2457 Shows some statistical data of closed positions. 2458 2459 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2460 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2461 :param show: if `True` then also prints all records to the console. 2462 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2463 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2464 :return: original list of dictionaries with history of deals records from API ("operations" key): 2465 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2466 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2467 """ 2468 if self.accountId is None or not self.accountId: 2469 uLogger.error("Variable `accountId` must be defined for using this method!") 2470 raise Exception("Account ID required") 2471 2472 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2473 2474 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2475 2476 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2477 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2478 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2479 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2480 customStat = {} # custom statistics in additional to responseJSON 2481 2482 # --- output report in human-readable format: 2483 if self.reportFile and (show or onlyFiles): 2484 splitLine1 = "| | | | | |\n" # Summary section 2485 splitLine2 = "| | | | | | | | |\n" # Operations section 2486 nextDay = "" 2487 2488 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2489 2490 if len(ops) > 0: 2491 customStat = { 2492 "opsCount": 0, # total operations count 2493 "buyCount": 0, # buy operations 2494 "sellCount": 0, # sell operations 2495 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2496 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2497 "payIn": {"rub": 0.}, # Deposit brokerage account 2498 "payOut": {"rub": 0.}, # Withdrawals 2499 "divs": {"rub": 0.}, # Dividends income 2500 "coupons": {"rub": 0.}, # Coupon's income 2501 "brokerCom": {"rub": 0.}, # Service commissions 2502 "serviceCom": {"rub": 0.}, # Service commissions 2503 "marginCom": {"rub": 0.}, # Margin commissions 2504 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2505 } 2506 2507 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2508 for item in ops: 2509 if item["state"] == "OPERATION_STATE_EXECUTED": 2510 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2511 2512 # count buy operations: 2513 if "_BUY" in item["operationType"]: 2514 customStat["buyCount"] += 1 2515 2516 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2517 customStat["buyTotal"][item["payment"]["currency"]] += payment 2518 2519 else: 2520 customStat["buyTotal"][item["payment"]["currency"]] = payment 2521 2522 # count sell operations: 2523 elif "_SELL" in item["operationType"]: 2524 customStat["sellCount"] += 1 2525 2526 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2527 customStat["sellTotal"][item["payment"]["currency"]] += payment 2528 2529 else: 2530 customStat["sellTotal"][item["payment"]["currency"]] = payment 2531 2532 # count incoming operations: 2533 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2534 if item["payment"]["currency"] in customStat["payIn"].keys(): 2535 customStat["payIn"][item["payment"]["currency"]] += payment 2536 2537 else: 2538 customStat["payIn"][item["payment"]["currency"]] = payment 2539 2540 # count withdrawals operations: 2541 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2542 if item["payment"]["currency"] in customStat["payOut"].keys(): 2543 customStat["payOut"][item["payment"]["currency"]] += payment 2544 2545 else: 2546 customStat["payOut"][item["payment"]["currency"]] = payment 2547 2548 # count dividends income: 2549 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2550 if item["payment"]["currency"] in customStat["divs"].keys(): 2551 customStat["divs"][item["payment"]["currency"]] += payment 2552 2553 else: 2554 customStat["divs"][item["payment"]["currency"]] = payment 2555 2556 # count coupon's income: 2557 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2558 if item["payment"]["currency"] in customStat["coupons"].keys(): 2559 customStat["coupons"][item["payment"]["currency"]] += payment 2560 2561 else: 2562 customStat["coupons"][item["payment"]["currency"]] = payment 2563 2564 # count broker commissions: 2565 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2566 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2567 customStat["brokerCom"][item["payment"]["currency"]] += payment 2568 2569 else: 2570 customStat["brokerCom"][item["payment"]["currency"]] = payment 2571 2572 # count service commissions: 2573 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2574 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2575 customStat["serviceCom"][item["payment"]["currency"]] += payment 2576 2577 else: 2578 customStat["serviceCom"][item["payment"]["currency"]] = payment 2579 2580 # count margin commissions: 2581 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2582 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2583 customStat["marginCom"][item["payment"]["currency"]] += payment 2584 2585 else: 2586 customStat["marginCom"][item["payment"]["currency"]] = payment 2587 2588 # count withholding taxes: 2589 elif "_TAX" in item["operationType"]: 2590 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2591 customStat["allTaxes"][item["payment"]["currency"]] += payment 2592 2593 else: 2594 customStat["allTaxes"][item["payment"]["currency"]] = payment 2595 2596 else: 2597 continue 2598 2599 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2600 2601 # --- view "Actions" lines: 2602 info.extend([ 2603 "| Report sections | | | | |\n", 2604 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2605 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2606 "| | Buy: {:<22} | {:<28} | | |\n".format( 2607 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2608 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2609 ), 2610 "| | Sell: {:<21} | {:<28} | | |\n".format( 2611 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2612 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2613 ), 2614 ]) 2615 2616 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2617 for key in opsKeys: 2618 if key == "rub": 2619 continue 2620 2621 info.extend([ 2622 "| | | {:<28} | | |\n".format( 2623 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2624 ), 2625 "| | | {:<28} | | |\n".format( 2626 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2627 ), 2628 ]) 2629 2630 info.append(splitLine1) 2631 2632 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2633 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2634 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2635 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2636 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2637 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2638 ) 2639 2640 # --- view "Payments" lines: 2641 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2642 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2643 2644 for key in paymentsKeys: 2645 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2646 2647 info.append(splitLine1) 2648 2649 # --- view "Commissions and taxes" lines: 2650 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2651 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2652 2653 for key in comKeys: 2654 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2655 2656 info.extend([ 2657 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2658 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2659 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2660 ]) 2661 2662 else: 2663 info.append("Broker returned no operations during this period\n") 2664 2665 # --- view "Operations" section: 2666 for item in ops: 2667 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2668 continue 2669 2670 else: 2671 self._figi = item["figi"] 2672 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2673 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2674 2675 # group of deals during one day: 2676 if nextDay and item["date"].split("T")[0] != nextDay: 2677 info.append(splitLine2) 2678 nextDay = "" 2679 2680 else: 2681 nextDay = item["date"].split("T")[0] # saving current day for splitting 2682 2683 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2684 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2685 self._figi if self._figi else "—", 2686 instrument["ticker"] if instrument else "—", 2687 instrument["type"] if instrument else "—", 2688 item["quantity"] if int(item["quantity"]) > 0 else "—", 2689 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2690 TKS_OPERATION_STATES[item["state"]], 2691 TKS_OPERATION_TYPES[item["operationType"]], 2692 )) 2693 2694 infoText = "".join(info) 2695 2696 if show and not onlyFiles: 2697 if self.moreDebug: 2698 uLogger.debug("Records about history of a client's operations successfully received") 2699 2700 uLogger.info(infoText) 2701 2702 if self.reportFile and (show or onlyFiles): 2703 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2704 fH.write(infoText) 2705 2706 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2707 2708 if self.useHTMLReports: 2709 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2710 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2711 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2712 2713 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2714 2715 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2717 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame: 2718 """ 2719 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2720 2721 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2722 Warning! Broker server used ISO UTC time by default. 2723 2724 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2725 Also, `historyFile` used to update history with `onlyMissing` parameter. 2726 2727 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2728 2729 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2730 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2731 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2732 `"hour"`, `"day"`. Default: `"hour"`. 2733 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2734 False by default. Warning! History appends only from last candle to current time 2735 with always update last candle! 2736 :param csvSep: separator if csv-file is used, `,` by default. 2737 :param show: if `True` then also prints Pandas DataFrame to the console. 2738 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2739 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2740 `["date", "time", "open", "high", "low", "close", "volume"]`. 2741 """ 2742 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2743 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2744 history = None # empty pandas object for history 2745 2746 if interval not in TKS_CANDLE_INTERVALS.keys(): 2747 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2748 raise Exception("Incorrect value") 2749 2750 if not (self._ticker or self._figi): 2751 uLogger.error("Ticker or FIGI must be defined!") 2752 raise Exception("Ticker or FIGI required") 2753 2754 if self._ticker and not self._figi: 2755 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2756 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2757 2758 if self._figi and not self._ticker: 2759 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2760 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2761 2762 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2763 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2764 if interval.lower() != "day": 2765 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2766 2767 delta = dtEnd - dtStart # current UTC time minus last time in file 2768 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2769 2770 # calculate history length in candles: 2771 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2772 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2773 length += 1 # to avoid fraction time 2774 2775 # calculate data blocks count: 2776 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2777 2778 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2779 if self.moreDebug: 2780 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2781 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2782 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2783 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2784 2785 tempOld = None # pandas object for old history, if --only-missing key present 2786 lastTime = None # datetime object of last old candle in file 2787 2788 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2789 if self.moreDebug: 2790 uLogger.debug("--only-missing key present, add only last missing candles...") 2791 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2792 2793 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2794 2795 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2796 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2797 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2798 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2799 2800 # get last datetime object from last string in file or minus 1 delta if file is empty: 2801 if len(tempOld) > 0: 2802 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2803 2804 else: 2805 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2806 2807 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2808 2809 responseJSONs = [] # raw history blocks of data 2810 2811 blockEnd = dtEnd 2812 for item in range(blocks): 2813 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2814 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2815 2816 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2817 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2818 )) 2819 2820 if blockStart == blockEnd: 2821 uLogger.debug("Skipped this zero-length block...") 2822 2823 else: 2824 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2825 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2826 self.body = str({ 2827 "figi": self._figi, 2828 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2829 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2830 "interval": TKS_CANDLE_INTERVALS[interval][0] 2831 }) 2832 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2833 2834 if "code" in responseJSON.keys(): 2835 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2836 2837 else: 2838 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2839 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2840 2841 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2842 2843 blockEnd = blockStart 2844 2845 printCount = len(responseJSONs) # candles to show in console 2846 if responseJSONs: 2847 tempHistory = pd.DataFrame( 2848 data={ 2849 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2850 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2851 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2852 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2853 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2854 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2855 "volume": [int(item["volume"]) for item in responseJSONs], 2856 }, 2857 index=range(len(responseJSONs)), 2858 columns=["date", "time", "open", "high", "low", "close", "volume"], 2859 ) 2860 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2861 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2862 2863 # append only newest candles to old history if --only-missing key present: 2864 if onlyMissing and tempOld is not None and lastTime is not None: 2865 index = 0 # find start index in tempHistory data: 2866 2867 for i, item in tempHistory.iterrows(): 2868 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2869 2870 if curTime == lastTime: 2871 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2872 index = i 2873 printCount = index + 1 2874 break 2875 2876 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2877 2878 else: 2879 history = tempHistory # if no `--only-missing` key then load full data from server 2880 2881 if self.moreDebug: 2882 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2883 2884 if history is not None and not history.empty: 2885 if show and not onlyFiles: 2886 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2887 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2888 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2889 )) 2890 2891 else: 2892 uLogger.warning("Received an empty candles history!") 2893 2894 if self.historyFile is not None: 2895 if history is not None and not history.empty: 2896 history.to_csv(self.historyFile, sep=csvSep, index=False, header=False) 2897 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2898 2899 else: 2900 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2901 2902 else: 2903 if self.moreDebug: 2904 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2905 2906 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2908 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2909 """ 2910 Load candles history from csv-file and return Pandas DataFrame object. 2911 2912 See also: `History()` and `ShowHistoryChart()` methods. 2913 2914 :param filePath: path to csv-file to open. 2915 """ 2916 loadedHistory = None # init candles data object 2917 2918 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2919 2920 if os.path.exists(filePath): 2921 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2922 2923 tfStr = self.priceModel.FormattedDelta( 2924 self.priceModel.timeframe, 2925 "{days} days {hours}h {minutes}m {seconds}s", 2926 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2927 self.priceModel.timeframe, 2928 "{hours}h {minutes}m {seconds}s", 2929 ) 2930 2931 if loadedHistory is not None and not loadedHistory.empty: 2932 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2933 len(loadedHistory), 2934 tfStr, 2935 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2936 ) 2937 2938 else: 2939 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2940 2941 else: 2942 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2943 2944 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2946 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2947 """ 2948 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2949 2950 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2951 Default: `index.html` (both for interact and non-interact candlesticks chart). 2952 2953 See also: `History()` and `LoadHistory()` methods. 2954 2955 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2956 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2957 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2958 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2959 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2960 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2961 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2962 """ 2963 if isinstance(candles, str): 2964 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2965 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2966 2967 elif isinstance(candles, pd.DataFrame): 2968 self.priceModel.prices = candles # set candles chain from variable 2969 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2970 2971 if "datetime" not in candles.columns: 2972 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2973 2974 else: 2975 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2976 raise Exception("Incorrect value") 2977 2978 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2979 2980 if interact: 2981 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2982 2983 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2984 2985 else: 2986 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2987 2988 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2989 2990 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2992 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2993 """ 2994 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2995 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2996 2997 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2998 2999 :param operation: string "Buy" or "Sell". 3000 :param lots: volume, integer count of lots >= 1. 3001 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 3002 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 3003 :param expDate: string "Undefined" by default or local date in future, 3004 it is a string with format `%Y-%m-%d %H:%M:%S`. 3005 :return: JSON with response from broker server. 3006 """ 3007 if self.accountId is None or not self.accountId: 3008 uLogger.error("Variable `accountId` must be defined for using this method!") 3009 raise Exception("Account ID required") 3010 3011 if operation is None or not operation or operation not in ("Buy", "Sell"): 3012 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3013 raise Exception("Incorrect value") 3014 3015 if lots is None or lots < 1: 3016 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 3017 lots = 1 3018 3019 if tp is None or tp < 0: 3020 tp = 0 3021 3022 if sl is None or sl < 0: 3023 sl = 0 3024 3025 if expDate is None or not expDate: 3026 expDate = "Undefined" 3027 3028 if not (self._ticker or self._figi): 3029 uLogger.error("Ticker or FIGI must be defined!") 3030 raise Exception("Ticker or FIGI required") 3031 3032 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3033 self._ticker = instrument["ticker"] 3034 self._figi = instrument["figi"] 3035 3036 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 3037 3038 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3039 self.body = str({ 3040 "figi": self._figi, 3041 "quantity": str(lots), 3042 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3043 "accountId": str(self.accountId), 3044 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3045 }) 3046 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3047 3048 if "orderId" in response.keys(): 3049 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3050 operation, response["orderId"], 3051 self._ticker, self._figi, lots, 3052 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3053 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3054 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3055 )) 3056 3057 if tp > 0: 3058 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3059 3060 if sl > 0: 3061 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3062 3063 else: 3064 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3065 3066 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3068 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3069 """ 3070 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3071 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3072 3073 See also: `Order()` and `Trade()` docstrings. 3074 3075 :param lots: volume, integer count of lots >= 1. 3076 :param tp: float > 0, take profit price of stop-order. 3077 :param sl: float > 0, stop loss price of stop-order. 3078 :param expDate: it's a local date in future. 3079 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3080 :return: JSON with response from broker server. 3081 """ 3082 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3084 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3085 """ 3086 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3087 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3088 3089 See also: `Order()` and `Trade()` docstrings. 3090 3091 :param lots: volume, integer count of lots >= 1. 3092 :param tp: float > 0, take profit price of stop-order. 3093 :param sl: float > 0, stop loss price of stop-order. 3094 :param expDate: it's a local date in the future. 3095 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3096 :return: JSON with response from broker server. 3097 """ 3098 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3100 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3101 """ 3102 Close position of given instruments. 3103 3104 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3105 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3106 This avoids unnecessary downloading data from the server. 3107 """ 3108 if instruments is None or not instruments: 3109 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3110 raise Exception("Ticker or FIGI required") 3111 3112 if isinstance(instruments, str): 3113 instruments = [instruments] 3114 3115 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3116 if uniqueInstruments: 3117 if portfolio is None or not portfolio: 3118 portfolio = self.Overview(show=False) 3119 3120 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3121 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3122 3123 for self._figi in uniqueInstruments: 3124 if self._figi not in allOpened: 3125 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3126 continue 3127 3128 # search open trade info about instrument by ticker: 3129 instrument = {} 3130 for iType in TKS_INSTRUMENTS: 3131 if instrument: 3132 break 3133 3134 for item in portfolio["stat"][iType]: 3135 if item["figi"] == self._figi: 3136 instrument = item 3137 break 3138 3139 if instrument: 3140 self._ticker = instrument["ticker"] 3141 self._figi = instrument["figi"] 3142 3143 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3144 self._ticker, 3145 self._figi, 3146 int(instrument["volume"]), 3147 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3148 )) 3149 3150 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3151 3152 if tradeLots > 0: 3153 if instrument["blocked"] > 0: 3154 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3155 instrument["blocked"], 3156 self._ticker, 3157 tradeLots, 3158 )) 3159 3160 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3161 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3162 3163 else: 3164 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3166 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3167 """ 3168 Close all positions of given instruments with defined type. 3169 3170 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3171 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3172 This avoids unnecessary downloading data from the server. 3173 """ 3174 if iType not in TKS_INSTRUMENTS: 3175 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3176 3177 else: 3178 if portfolio is None or not portfolio: 3179 portfolio = self.Overview(show=False) 3180 3181 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3182 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3183 3184 if tickers and portfolio: 3185 self.CloseTrades(tickers, portfolio) 3186 3187 else: 3188 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3190 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3191 """ 3192 Universal method to create market or limit orders with all available parameters for current `accountId`. 3193 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3194 3195 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3196 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3197 3198 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3199 then broker immediately open market order as you can do simple --buy or --sell operations! 3200 3201 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3202 When current price will go up or down to target price value then broker opens a limit order. 3203 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3204 3205 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3206 3207 :param operation: string "Buy" or "Sell". 3208 :param orderType: string "Limit" or "Stop". 3209 :param lots: volume, integer count of lots >= 1. 3210 :param targetPrice: target price > 0. This is open trade price for limit order. 3211 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3212 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3213 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3214 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3215 Stop loss order always executed by market price. 3216 :param expDate: string "Undefined" by default or local date in future. 3217 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3218 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3219 A limit order has no expiration date, it lasts until the end of the trading day. 3220 :return: JSON with response from broker server. 3221 """ 3222 if self.accountId is None or not self.accountId: 3223 uLogger.error("Variable `accountId` must be defined for using this method!") 3224 raise Exception("Account ID required") 3225 3226 if operation is None or not operation or operation not in ("Buy", "Sell"): 3227 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3228 raise Exception("Incorrect value") 3229 3230 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3231 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3232 raise Exception("Incorrect value") 3233 3234 if lots is None or lots < 1: 3235 uLogger.error("You must define trade volume > 0: integer count of lots!") 3236 raise Exception("Incorrect value") 3237 3238 if targetPrice is None or targetPrice <= 0: 3239 uLogger.error("Target price for limit-order must be greater than 0!") 3240 raise Exception("Incorrect value") 3241 3242 if limitPrice is None or limitPrice <= 0: 3243 limitPrice = targetPrice 3244 3245 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3246 stopType = "Limit" 3247 3248 if expDate is None or not expDate: 3249 expDate = "Undefined" 3250 3251 if not (self._ticker or self._figi): 3252 uLogger.error("Tocker or FIGI must be defined!") 3253 raise Exception("Ticker or FIGI required") 3254 3255 response = {} 3256 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3257 self._ticker = instrument["ticker"] 3258 self._figi = instrument["figi"] 3259 3260 if orderType == "Limit": 3261 uLogger.debug( 3262 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3263 self._ticker, self._figi, 3264 operation, lots, targetPrice, instrument["currency"], 3265 )) 3266 3267 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3268 self.body = str({ 3269 "figi": self._figi, 3270 "quantity": str(lots), 3271 "price": FloatToNano(targetPrice), 3272 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3273 "accountId": str(self.accountId), 3274 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3275 }) 3276 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3277 3278 if "orderId" in response.keys(): 3279 uLogger.info( 3280 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3281 response["orderId"], self._ticker, self._figi, operation, lots, 3282 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3283 )) 3284 3285 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3286 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3287 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3288 targetPrice, instrument["currency"], 3289 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3290 )) 3291 3292 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3293 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3294 targetPrice, instrument["currency"], 3295 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3296 )) 3297 3298 else: 3299 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3300 3301 if orderType == "Stop": 3302 uLogger.debug( 3303 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3304 self._ticker, self._figi, 3305 operation, lots, 3306 targetPrice, instrument["currency"], 3307 limitPrice, instrument["currency"], 3308 stopType, expDate, 3309 )) 3310 3311 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3312 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3313 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3314 3315 body = { 3316 "figi": self._figi, 3317 "quantity": str(lots), 3318 "price": FloatToNano(limitPrice), 3319 "stopPrice": FloatToNano(targetPrice), 3320 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3321 "accountId": str(self.accountId), 3322 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3323 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3324 } 3325 3326 if expDateUTC: 3327 body["expireDate"] = expDateUTC 3328 3329 self.body = str(body) 3330 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3331 3332 if "stopOrderId" in response.keys(): 3333 uLogger.info( 3334 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3335 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3336 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3337 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3338 TKS_STOP_ORDER_TYPES[stopOrderType], 3339 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3340 )) 3341 3342 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3343 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3344 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{} {}] is lower than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3345 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3346 "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"], 3347 )) 3348 3349 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3350 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{} {}] is higher than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3351 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3352 "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"], 3353 )) 3354 3355 else: 3356 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3357 3358 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3360 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3361 """ 3362 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3363 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3364 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3365 See also: `Order()` docstring. 3366 3367 :param lots: volume, integer count of lots >= 1. 3368 :param targetPrice: target price > 0. This is open trade price for limit order. 3369 :return: JSON with response from broker server. 3370 """ 3371 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3373 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3374 """ 3375 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3376 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3377 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3378 target price value then broker opens a limit order. See also: `Order()` docstring. 3379 3380 :param lots: volume, integer count of lots >= 1. 3381 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3382 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3383 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3384 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3385 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3386 :param expDate: string "Undefined" by default or local date in future. 3387 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3388 This date is converting to UTC format for server. 3389 :return: JSON with response from broker server. 3390 """ 3391 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3393 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3394 """ 3395 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3396 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3397 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3398 See also: `Order()` docstring. 3399 3400 :param lots: volume, integer count of lots >= 1. 3401 :param targetPrice: target price > 0. This is open trade price for limit order. 3402 :return: JSON with response from broker server. 3403 """ 3404 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3406 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3407 """ 3408 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3409 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3410 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3411 target price value then broker opens a limit order. See also: `Order()` docstring. 3412 3413 :param lots: volume, integer count of lots >= 1. 3414 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3415 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3416 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3417 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3418 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3419 :param expDate: string "Undefined" by default or local date in future. 3420 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3421 This date is converting to UTC format for server. 3422 :return: JSON with response from broker server. 3423 """ 3424 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3426 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3427 """ 3428 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3429 3430 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3431 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3432 This avoids unnecessary downloading data from the server. 3433 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3434 """ 3435 if self.accountId is None or not self.accountId: 3436 uLogger.error("Variable `accountId` must be defined for using this method!") 3437 raise Exception("Account ID required") 3438 3439 if orderIDs: 3440 if allOrdersIDs is None: 3441 rawOrders = self.RequestPendingOrders() 3442 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3443 3444 if allStopOrdersIDs is None: 3445 rawStopOrders = self.RequestStopOrders() 3446 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3447 3448 for orderID in orderIDs: 3449 idInPendingOrders = orderID in allOrdersIDs 3450 idInStopOrders = orderID in allStopOrdersIDs 3451 3452 if not (idInPendingOrders or idInStopOrders): 3453 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3454 continue 3455 3456 else: 3457 if idInPendingOrders: 3458 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3459 3460 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3461 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3462 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3463 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3464 3465 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3466 if self.moreDebug: 3467 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3468 3469 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3470 3471 else: 3472 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3473 3474 elif idInStopOrders: 3475 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3476 3477 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3478 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3479 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3480 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3481 3482 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3483 if self.moreDebug: 3484 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3485 3486 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3487 3488 else: 3489 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3490 3491 else: 3492 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3494 def CloseAllOrders(self) -> None: 3495 """ 3496 Gets a list of open pending and stop orders and cancel it all. 3497 """ 3498 rawOrders = self.RequestPendingOrders() 3499 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3500 lenOrders = len(allOrdersIDs) 3501 3502 rawStopOrders = self.RequestStopOrders() 3503 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3504 lenSOrders = len(allStopOrdersIDs) 3505 3506 if lenOrders > 0 or lenSOrders > 0: 3507 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3508 3509 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3510 3511 else: 3512 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3514 def CloseAll(self, *args) -> None: 3515 """ 3516 Close all available (not blocked) opened trades and orders. 3517 3518 Also, you can select one or more keywords case-insensitive: 3519 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3520 3521 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3522 """ 3523 overview = self.Overview(show=False) # get all open trades info 3524 3525 if len(args) == 0: 3526 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3527 self.CloseAllOrders() # close all pending and stop orders 3528 3529 for iType in TKS_INSTRUMENTS: 3530 if iType != "Currencies": 3531 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3532 3533 else: 3534 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3535 lowerArgs = [x.lower() for x in args] 3536 3537 if "orders" in lowerArgs: 3538 self.CloseAllOrders() # close all pending and stop orders 3539 3540 for iType in TKS_INSTRUMENTS: 3541 if iType.lower() in lowerArgs and iType != "Currencies": 3542 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3544 def CloseAllByTicker(self, instrument: str) -> None: 3545 """ 3546 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3547 3548 This method searches opened trade and orders of instrument throw all portfolio and then use 3549 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3550 3551 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3552 3553 :param instrument: string with ticker. 3554 """ 3555 if instrument is None or not instrument: 3556 uLogger.error("Ticker name must be defined for using this method!") 3557 raise Exception("Ticker required") 3558 3559 overview = self.Overview(show=False) # get user portfolio with all open trades info 3560 3561 self._ticker = instrument # try to set instrument as ticker 3562 self._figi = "" 3563 3564 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3565 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3566 3567 if limitAll and self.IsInLimitOrders(portfolio=overview): 3568 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3569 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3570 3571 if stopAll and self.IsInStopOrders(portfolio=overview): 3572 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3573 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3574 3575 if self.IsInPortfolio(portfolio=overview): 3576 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3577 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with ticker.
3579 def CloseAllByFIGI(self, instrument: str) -> None: 3580 """ 3581 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3582 3583 This method searches opened trade and orders of instrument throw all portfolio and then use 3584 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3585 3586 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3587 3588 :param instrument: string with FIGI id. 3589 """ 3590 if instrument is None or not instrument: 3591 uLogger.error("FIGI id must be defined for using this method!") 3592 raise Exception("FIGI required") 3593 3594 overview = self.Overview(show=False) # get user portfolio with all open trades info 3595 3596 self._ticker = "" 3597 self._figi = instrument # try to set instrument as FIGI id 3598 3599 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3600 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3601 3602 if limitAll and self.IsInLimitOrders(portfolio=overview): 3603 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3604 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3605 3606 if stopAll and self.IsInStopOrders(portfolio=overview): 3607 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3608 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3609 3610 if self.IsInPortfolio(portfolio=overview): 3611 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3612 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with FIGI id.
3614 @staticmethod 3615 def ParseOrderParameters(operation, **inputParameters): 3616 """ 3617 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3618 3619 :param operation: string "Buy" or "Sell". 3620 :param inputParameters: this is dict of strings that looks like this 3621 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3622 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3623 "prices" key: one or more prices to open limit-orders 3624 Counts of values in lots and prices lists must be equals! 3625 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3626 """ 3627 # TODO: update order grid work with api v2 3628 pass 3629 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3630 # 3631 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3632 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3633 # raise Exception("Incorrect value") 3634 # 3635 # if "l" in inputParameters.keys(): 3636 # inputParameters["lots"] = inputParameters.pop("l") 3637 # 3638 # if "p" in inputParameters.keys(): 3639 # inputParameters["prices"] = inputParameters.pop("p") 3640 # 3641 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3642 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3643 # raise Exception("Incorrect value") 3644 # 3645 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3646 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3647 # 3648 # if len(lots) != len(prices): 3649 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3650 # raise Exception("Incorrect value") 3651 # 3652 # uLogger.debug("Extracted parameters for orders:") 3653 # uLogger.debug("lots = {}".format(lots)) 3654 # uLogger.debug("prices = {}".format(prices)) 3655 # 3656 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3657 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3658 # uLogger.debug("Order parameters: {}".format(result)) 3659 # 3660 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3662 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3663 """ 3664 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3665 3666 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3667 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3668 """ 3669 result = False 3670 msg = "Instrument not defined!" 3671 3672 if portfolio is None or not portfolio: 3673 portfolio = self.Overview(show=False) 3674 3675 if self._ticker: 3676 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3677 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3678 3679 for iType in TKS_INSTRUMENTS: 3680 for instrument in portfolio["stat"][iType]: 3681 if instrument["ticker"] == self._ticker: 3682 result = True 3683 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3684 break 3685 3686 elif self._figi: 3687 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3688 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3689 3690 for iType in TKS_INSTRUMENTS: 3691 for instrument in portfolio["stat"][iType]: 3692 if instrument["figi"] == self._figi: 3693 result = True 3694 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3695 break 3696 3697 else: 3698 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3699 3700 uLogger.debug(msg) 3701 3702 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3704 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3705 """ 3706 Returns instrument from the user's portfolio if it presents there. 3707 Instrument must be defined by `ticker` (highly priority) or `figi`. 3708 3709 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3710 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3711 """ 3712 result = None 3713 msg = "Instrument not defined!" 3714 3715 if portfolio is None or not portfolio: 3716 portfolio = self.Overview(show=False) 3717 3718 if self._ticker: 3719 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3720 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3721 3722 for iType in TKS_INSTRUMENTS: 3723 for instrument in portfolio["stat"][iType]: 3724 if instrument["ticker"] == self._ticker: 3725 result = instrument 3726 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3727 break 3728 3729 elif self._figi: 3730 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3731 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3732 3733 for iType in TKS_INSTRUMENTS: 3734 for instrument in portfolio["stat"][iType]: 3735 if instrument["figi"] == self._figi: 3736 result = instrument 3737 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3738 break 3739 3740 else: 3741 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3742 3743 uLogger.debug(msg) 3744 3745 return result
Returns instrument from the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3747 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3748 """ 3749 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3750 3751 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3752 3753 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3754 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3755 """ 3756 result = False 3757 msg = "Instrument not defined!" 3758 3759 if portfolio is None or not portfolio: 3760 portfolio = self.Overview(show=False) 3761 3762 if self._ticker: 3763 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3764 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3765 3766 for instrument in portfolio["stat"]["orders"]: 3767 if instrument["ticker"] == self._ticker: 3768 result = True 3769 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3770 break 3771 3772 elif self._figi: 3773 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3774 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3775 3776 for instrument in portfolio["stat"]["orders"]: 3777 if instrument["figi"] == self._figi: 3778 result = True 3779 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3780 break 3781 3782 else: 3783 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3784 3785 uLogger.debug(msg) 3786 3787 return result
Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif limit orders list contains some limit orders for the instrument,Falseotherwise.
3789 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3790 """ 3791 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3792 Instrument must be defined by `ticker` (highly priority) or `figi`. 3793 3794 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3795 3796 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3797 :return: list with `orderID`s of limit orders. 3798 """ 3799 result = [] 3800 msg = "Instrument not defined!" 3801 3802 if portfolio is None or not portfolio: 3803 portfolio = self.Overview(show=False) 3804 3805 if self._ticker: 3806 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3807 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3808 3809 for instrument in portfolio["stat"]["orders"]: 3810 if instrument["ticker"] == self._ticker: 3811 result.append(instrument["orderID"]) 3812 3813 if result: 3814 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3815 3816 elif self._figi: 3817 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3818 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3819 3820 for instrument in portfolio["stat"]["orders"]: 3821 if instrument["figi"] == self._figi: 3822 result.append(instrument["orderID"]) 3823 3824 if result: 3825 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3826 3827 else: 3828 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3829 3830 uLogger.debug(msg) 3831 3832 return result
Returns list with all orderIDs of opened pending limit orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of limit orders.
3834 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3835 """ 3836 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3837 3838 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3839 3840 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3841 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3842 """ 3843 result = False 3844 msg = "Instrument not defined!" 3845 3846 if portfolio is None or not portfolio: 3847 portfolio = self.Overview(show=False) 3848 3849 if self._ticker: 3850 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3851 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3852 3853 for instrument in portfolio["stat"]["stopOrders"]: 3854 if instrument["ticker"] == self._ticker: 3855 result = True 3856 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3857 break 3858 3859 elif self._figi: 3860 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3861 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3862 3863 for instrument in portfolio["stat"]["stopOrders"]: 3864 if instrument["figi"] == self._figi: 3865 result = True 3866 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3867 break 3868 3869 else: 3870 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3871 3872 uLogger.debug(msg) 3873 3874 return result
Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif stop orders list contains some stop orders for the instrument,Falseotherwise.
3876 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3877 """ 3878 Returns list with all `orderID`s of opened stop orders for the instrument. 3879 Instrument must be defined by `ticker` (highly priority) or `figi`. 3880 3881 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3882 3883 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3884 :return: list with `orderID`s of stop orders. 3885 """ 3886 result = [] 3887 msg = "Instrument not defined!" 3888 3889 if portfolio is None or not portfolio: 3890 portfolio = self.Overview(show=False) 3891 3892 if self._ticker: 3893 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3894 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3895 3896 for instrument in portfolio["stat"]["stopOrders"]: 3897 if instrument["ticker"] == self._ticker: 3898 result.append(instrument["orderID"]) 3899 3900 if result: 3901 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3902 3903 elif self._figi: 3904 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3905 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3906 3907 for instrument in portfolio["stat"]["stopOrders"]: 3908 if instrument["figi"] == self._figi: 3909 result.append(instrument["orderID"]) 3910 3911 if result: 3912 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3913 3914 else: 3915 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3916 3917 uLogger.debug(msg) 3918 3919 return result
Returns list with all orderIDs of opened stop orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of stop orders.
3921 def RequestLimits(self) -> dict: 3922 """ 3923 Method for obtaining the available funds for withdrawal for current `accountId`. 3924 3925 See also: 3926 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3927 - `OverviewLimits()` method 3928 3929 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3930 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3931 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3932 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3933 """ 3934 if self.accountId is None or not self.accountId: 3935 uLogger.error("Variable `accountId` must be defined for using this method!") 3936 raise Exception("Account ID required") 3937 3938 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3939 3940 self.body = str({"accountId": self.accountId}) 3941 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3942 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3943 3944 if self.moreDebug: 3945 uLogger.debug("Records about available funds for withdrawal successfully received") 3946 3947 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3949 def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict: 3950 """ 3951 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3952 3953 See also: `RequestLimits()`. 3954 3955 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3956 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 3957 :return: dict with raw parsed data from server and some calculated statistics about it. 3958 """ 3959 if self.accountId is None or not self.accountId: 3960 uLogger.error("Variable `accountId` must be defined for using this method!") 3961 raise Exception("Account ID required") 3962 3963 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3964 3965 view = { 3966 "rawLimits": rawLimits, 3967 "limits": { # parsed data for every currency: 3968 "money": { # this is an array of portfolio currency positions 3969 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3970 }, 3971 "blocked": { # this is an array of blocked currency 3972 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3973 }, 3974 "blockedGuarantee": { # this is locked money under collateral for futures 3975 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3976 }, 3977 }, 3978 } 3979 3980 # --- Prepare text table with limits in human-readable format: 3981 if show or onlyFiles: 3982 info = [ 3983 "# Withdrawal limits\n\n", 3984 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3985 "* **Account ID:** [{}]\n".format(self.accountId), 3986 ] 3987 3988 if view["limits"]["money"]: 3989 info.extend([ 3990 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3991 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3992 ]) 3993 3994 else: 3995 info.append("\nNo withdrawal limits\n") 3996 3997 for curr in view["limits"]["money"].keys(): 3998 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3999 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 4000 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 4001 4002 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 4003 "[{}]".format(curr), 4004 "{:.2f}".format(view["limits"]["money"][curr]), 4005 "{:.2f}".format(availableMoney), 4006 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 4007 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 4008 ) 4009 4010 if curr == "rub": 4011 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 4012 4013 else: 4014 info.append(infoStr) 4015 4016 infoText = "".join(info) 4017 4018 if show and not onlyFiles: 4019 uLogger.info(infoText) 4020 4021 if self.withdrawalLimitsFile and (show or onlyFiles): 4022 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 4023 fH.write(infoText) 4024 4025 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 4026 4027 if self.useHTMLReports: 4028 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 4029 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4030 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 4031 4032 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4033 4034 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4036 def RequestAccounts(self) -> dict: 4037 """ 4038 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 4039 4040 See also: 4041 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 4042 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4043 - `OverviewUserInfo()` method 4044 4045 :return: dict with raw data from server that contains accounts info. Example of dict: 4046 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4047 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4048 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4049 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4050 """ 4051 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4052 4053 self.body = str({}) 4054 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4055 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4056 4057 if self.moreDebug: 4058 uLogger.debug("Records about available accounts successfully received") 4059 4060 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
4062 def RequestUserInfo(self) -> dict: 4063 """ 4064 Method for requesting common user's information. 4065 4066 See also: 4067 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4068 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4069 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4070 - `OverviewUserInfo()` method 4071 4072 :return: dict with raw data from server that contains user's information. Example of dict: 4073 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4074 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4075 """ 4076 uLogger.debug("Requesting common user's information. Wait, please...") 4077 4078 self.body = str({}) 4079 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4080 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4081 4082 if self.moreDebug: 4083 uLogger.debug("Records about current user successfully received") 4084 4085 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
4087 def RequestMarginStatus(self, accountId: str = None) -> dict: 4088 """ 4089 Method for requesting margin calculation for defined account ID. 4090 4091 See also: 4092 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4093 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4094 - `OverviewUserInfo()` method 4095 4096 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4097 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4098 Example of responses: 4099 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4100 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4101 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4102 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4103 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4104 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4105 """ 4106 if accountId is None or not accountId: 4107 if self.accountId is None or not self.accountId: 4108 uLogger.error("Variable `accountId` must be defined for using this method!") 4109 raise Exception("Account ID required") 4110 4111 else: 4112 accountId = self.accountId # use `self.accountId` (main ID) by default 4113 4114 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4115 4116 self.body = str({"accountId": accountId}) 4117 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4118 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4119 4120 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4121 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4122 rawMargin = {} 4123 4124 else: 4125 if self.moreDebug: 4126 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4127 4128 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
4130 def RequestTariffLimits(self) -> dict: 4131 """ 4132 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4133 4134 See also: 4135 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4136 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4137 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4138 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4139 - `OverviewUserInfo()` method 4140 4141 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4142 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4143 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4144 """ 4145 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4146 4147 self.body = str({}) 4148 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4149 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4150 4151 if self.moreDebug: 4152 uLogger.debug("Records with limits of current tariff successfully received") 4153 4154 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
4156 def RequestBondCoupons(self, iJSON: dict) -> dict: 4157 """ 4158 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4159 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4160 All dates are in UTC timezone. 4161 4162 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4163 Documentation: 4164 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4165 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4166 4167 See also: `ExtendBondsData()`. 4168 4169 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4170 If raw iJSON is not data of bond then server returns an error [400] with message: 4171 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4172 :return: dictionary with bond payment calendar. Response example 4173 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4174 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4175 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4176 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4177 """ 4178 if iJSON["figi"] is None or not iJSON["figi"]: 4179 uLogger.error("FIGI must be defined for using this method!") 4180 raise Exception("FIGI required") 4181 4182 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4183 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4184 4185 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4186 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4187 self._figi, 4188 startDate, 4189 endDate, 4190 )) 4191 4192 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4193 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4194 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4195 4196 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4197 uLogger.warning("Instrument type is not bond!") 4198 4199 else: 4200 if self.moreDebug: 4201 uLogger.debug("Records about bond payment calendar successfully received") 4202 4203 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self._ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
4205 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4206 """ 4207 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4208 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4209 coupon yields, current yields and some statistics etc. 4210 4211 WARNING! This is too long operation if a lot of bonds requested from broker server. 4212 4213 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4214 4215 :param instruments: list of strings with tickers or FIGIs. 4216 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4217 for further used by data scientists or stock analytics. 4218 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4219 In XLSX-file and Pandas DataFrame fields mean: 4220 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4221 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4222 """ 4223 if instruments is None or not instruments: 4224 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4225 raise Exception("Ticker or FIGI required") 4226 4227 if isinstance(instruments, str): 4228 instruments = [instruments] 4229 4230 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4231 4232 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4233 4234 iCount = len(uniqueInstruments) 4235 tooLong = iCount >= 20 4236 if tooLong: 4237 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4238 4239 bonds = None 4240 for i, self._figi in enumerate(uniqueInstruments): 4241 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4242 4243 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4244 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4245 rawBond = self.SearchByFIGI(requestPrice=True) 4246 4247 # Widen raw data with UTC current time (iData["actualDateTime"]): 4248 actualDate = datetime.now(tzutc()) 4249 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4250 4251 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4252 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4253 4254 # Replace some values with human-readable: 4255 iData["nominalCurrency"] = iData["nominal"]["currency"] 4256 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4257 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4258 iData["aciCurrency"] = iData["aciValue"]["currency"] 4259 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4260 iData["issueSize"] = int(iData["issueSize"]) 4261 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4262 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4263 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4264 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4265 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4266 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4267 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4268 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4269 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4270 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4271 4272 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4273 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4274 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4275 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4276 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4277 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4278 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4279 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4280 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4281 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4282 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4283 4284 # Widen raw data with calendar data from `rawCalendar` values: 4285 calendarData = [] 4286 if "events" in iData["rawCalendar"].keys(): 4287 for item in iData["rawCalendar"]["events"]: 4288 calendarData.append({ 4289 "couponDate": item["couponDate"], 4290 "couponNumber": int(item["couponNumber"]), 4291 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4292 "payCurrency": item["payOneBond"]["currency"], 4293 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4294 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4295 "couponStartDate": item["couponStartDate"], 4296 "couponEndDate": item["couponEndDate"], 4297 "couponPeriod": item["couponPeriod"], 4298 }) 4299 4300 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4301 if "maturityDate" not in iData.keys(): 4302 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4303 4304 # Widen raw data with Coupon Rate. 4305 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4306 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4307 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4308 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4309 4310 # Widen raw data with Yield to Maturity (YTM) on current date. 4311 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4312 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4313 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4314 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4315 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4316 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4317 4318 iData["calendar"] = calendarData # adds calendar at the end 4319 4320 # Remove not used data: 4321 iData.pop("uid") 4322 iData.pop("positionUid") 4323 iData.pop("currentPrice") 4324 iData.pop("rawCalendar") 4325 4326 colNames = list(iData.keys()) 4327 if bonds is None: 4328 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4329 4330 else: 4331 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4332 4333 else: 4334 uLogger.warning("Instrument is not a bond!") 4335 4336 processed = round(100 * (i + 1) / iCount, 1) 4337 if tooLong and processed % 5 == 0: 4338 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4339 4340 else: 4341 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4342 4343 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4344 4345 # Saving bonds from Pandas DataFrame to XLSX sheet: 4346 if xlsx and self.bondsXLSXFile: 4347 with pd.ExcelWriter( 4348 path=self.bondsXLSXFile, 4349 date_format=TKS_DATE_FORMAT, 4350 datetime_format=TKS_DATE_TIME_FORMAT, 4351 mode="w", 4352 ) as writer: 4353 bonds.to_excel( 4354 writer, 4355 sheet_name="Extended bonds data", 4356 index=True, 4357 encoding="UTF-8", 4358 freeze_panes=(1, 1), 4359 ) # saving as XLSX-file with freeze first row and column as headers 4360 4361 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4362 4363 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4365 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4366 """ 4367 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4368 4369 WARNING! This is too long operation if a lot of bonds requested from broker server. 4370 4371 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4372 4373 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4374 extended information about bonds: main info, current prices, bond payment calendar, 4375 coupon yields, current yields and some statistics etc. 4376 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4377 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4378 for further used by data scientists or stock analytics. 4379 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4380 """ 4381 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4382 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4383 4384 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4385 4386 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4387 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4388 calendar = None 4389 for bond in extBonds.iterrows(): 4390 for item in bond[1]["calendar"]: 4391 cData = { 4392 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4393 "couponDate": item["couponDate"], 4394 "figi": bond[1]["figi"], 4395 "ticker": bond[1]["ticker"], 4396 "name": bond[1]["name"], 4397 "couponNumber": item["couponNumber"], 4398 "payOneBond": item["payOneBond"], 4399 "payCurrency": item["payCurrency"], 4400 "couponType": item["couponType"], 4401 "couponPeriod": item["couponPeriod"], 4402 "fixDate": item["fixDate"], 4403 "couponStartDate": item["couponStartDate"], 4404 "couponEndDate": item["couponEndDate"], 4405 } 4406 4407 if calendar is None: 4408 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4409 4410 else: 4411 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4412 4413 if calendar is not None: 4414 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4415 4416 # Saving calendar from Pandas DataFrame to XLSX sheet: 4417 if xlsx: 4418 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4419 4420 with pd.ExcelWriter( 4421 path=xlsxCalendarFile, 4422 date_format=TKS_DATE_FORMAT, 4423 datetime_format=TKS_DATE_TIME_FORMAT, 4424 mode="w", 4425 ) as writer: 4426 humanReadable = calendar.copy(deep=True) 4427 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4428 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4429 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4430 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4431 humanReadable.columns = colNames # human-readable column names 4432 4433 humanReadable.to_excel( 4434 writer, 4435 sheet_name="Bond payments calendar", 4436 index=False, 4437 encoding="UTF-8", 4438 freeze_panes=(1, 2), 4439 ) # saving as XLSX-file with freeze first row and column as headers 4440 4441 del humanReadable # release df in memory 4442 4443 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4444 4445 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4447 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str: 4448 """ 4449 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4450 Also, creates Markdown file with calendar data, `calendar.md` by default. 4451 4452 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4453 4454 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4455 extended information about bonds: main info, current prices, bond payment calendar, 4456 coupon yields, current yields and some statistics etc. 4457 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4458 :param show: if `True` then also printing bonds payment calendar to the console, 4459 otherwise save to file `calendarFile` only. `False` by default. 4460 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4461 :return: multilines text in Markdown format with bonds payment calendar as a table. 4462 """ 4463 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4464 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles) 4465 4466 infoText = "# Bond payments calendar\n\n" 4467 4468 calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles) # generate Pandas DataFrame with full calendar data 4469 4470 if not (calendar is None or calendar.empty): 4471 splitLine = "| | | | | | | | | |\n" 4472 4473 info = [ 4474 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4475 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4476 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4477 ] 4478 4479 newMonth = False 4480 notOneBond = calendar["figi"].nunique() > 1 4481 for i, bond in enumerate(calendar.iterrows()): 4482 if newMonth and notOneBond: 4483 info.append(splitLine) 4484 4485 info.append( 4486 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4487 " √" if bond[1]["paid"] else " —", 4488 bond[1]["couponDate"].split("T")[0], 4489 bond[1]["figi"], 4490 bond[1]["ticker"], 4491 bond[1]["couponNumber"], 4492 "{} {}".format( 4493 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4494 bond[1]["payCurrency"], 4495 ), 4496 bond[1]["couponType"], 4497 bond[1]["couponPeriod"], 4498 bond[1]["fixDate"].split("T")[0], 4499 ) 4500 ) 4501 4502 if i < len(calendar.values) - 1: 4503 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4504 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4505 newMonth = False if curDate.month == nextDate.month else True 4506 4507 else: 4508 newMonth = False 4509 4510 infoText += "".join(info) 4511 4512 if show and not onlyFiles: 4513 uLogger.info("{}".format(infoText)) 4514 4515 if self.calendarFile is not None and (show or onlyFiles): 4516 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4517 fH.write(infoText) 4518 4519 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4520 4521 if self.useHTMLReports: 4522 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4523 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4524 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4525 4526 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4527 4528 else: 4529 infoText += "No data\n" 4530 4531 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4533 def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict: 4534 """ 4535 Method for parsing and show simple table with all available user accounts. 4536 4537 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4538 4539 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4540 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4541 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4542 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4543 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4544 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4545 "closed": "—", "access": "Full access" }, ...}}` 4546 """ 4547 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4548 4549 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4550 accounts = { 4551 item["id"]: { 4552 "type": TKS_ACCOUNT_TYPES[item["type"]], 4553 "name": item["name"], 4554 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4555 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4556 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4557 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4558 } for item in rawAccounts["accounts"] 4559 } 4560 4561 # Raw and parsed data with some fields replaced in "stat" section: 4562 view = { 4563 "rawAccounts": rawAccounts, 4564 "stat": accounts, 4565 } 4566 4567 # --- Prepare simple text table with only accounts data in human-readable format: 4568 if show or onlyFiles: 4569 info = [ 4570 "# User accounts\n\n", 4571 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4572 "| Account ID | Type | Status | Name |\n", 4573 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4574 ] 4575 4576 for account in view["stat"].keys(): 4577 info.extend([ 4578 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4579 account, 4580 view["stat"][account]["type"], 4581 view["stat"][account]["status"], 4582 view["stat"][account]["name"], 4583 ) 4584 ]) 4585 4586 infoText = "".join(info) 4587 4588 if show and not onlyFiles: 4589 uLogger.info(infoText) 4590 4591 if self.userAccountsFile and (show or onlyFiles): 4592 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4593 fH.write(infoText) 4594 4595 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4596 4597 if self.useHTMLReports: 4598 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4599 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4600 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4601 4602 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4603 4604 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4606 def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict: 4607 """ 4608 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4609 4610 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4611 4612 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4613 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4614 :return: dict with raw parsed data from server and some calculated statistics about it. 4615 """ 4616 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4617 tmpTicker = self._ticker 4618 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4619 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4620 self._ticker = tmpTicker 4621 4622 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4623 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4624 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4625 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4626 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4627 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4628 4629 # This is dict with parsed common user data: 4630 userInfo = { 4631 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4632 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4633 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4634 "tariff": rawUserInfo["tariff"], 4635 } 4636 4637 # This is an array of dict with parsed margin statuses for every account IDs: 4638 margins = {} 4639 for accountId in accounts.keys(): 4640 if rawMargins[accountId]: 4641 margins[accountId] = { 4642 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4643 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4644 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4645 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4646 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4647 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4648 "missing": missing["volume"], 4649 } 4650 4651 else: 4652 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4653 4654 unary = {} # unary-connection limits 4655 for item in rawTariffLimits["unaryLimits"]: 4656 if item["limitPerMinute"] in unary.keys(): 4657 unary[item["limitPerMinute"]].extend(item["methods"]) 4658 4659 else: 4660 unary[item["limitPerMinute"]] = item["methods"] 4661 4662 stream = {} # stream-connection limits 4663 for item in rawTariffLimits["streamLimits"]: 4664 if item["limit"] in stream.keys(): 4665 stream[item["limit"]].extend(item["streams"]) 4666 4667 else: 4668 stream[item["limit"]] = item["streams"] 4669 4670 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4671 limits = { 4672 "unary": unary, 4673 "stream": stream, 4674 } 4675 4676 # Raw and parsed data as an output result: 4677 view = { 4678 "rawUserInfo": rawUserInfo, 4679 "rawAccounts": rawAccounts, 4680 "rawMargins": rawMargins, 4681 "rawTariffLimits": rawTariffLimits, 4682 "stat": { 4683 "overview": overview, 4684 "userInfo": userInfo, 4685 "accounts": accounts, 4686 "margins": margins, 4687 "limits": limits, 4688 }, 4689 } 4690 4691 # --- Prepare text table with user information in human-readable format: 4692 if show or onlyFiles: 4693 info = [ 4694 "# Full user information\n\n", 4695 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4696 "## Common information\n\n", 4697 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4698 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4699 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4700 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4701 "\n## User accounts\n\n", 4702 ] 4703 4704 for account in view["stat"]["accounts"].keys(): 4705 info.extend([ 4706 "### ID: [{}]\n\n".format(account), 4707 "| Parameters | Values |\n", 4708 "|----------------------|--------------------------------------------------------------|\n", 4709 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4710 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4711 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4712 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4713 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4714 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4715 ]) 4716 4717 if margins[account]: 4718 info.extend([ 4719 "| Margin status: | Enabled |\n", 4720 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4721 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4722 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4723 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4724 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4725 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4726 ]) 4727 4728 else: 4729 info.append("| Margin status: | Disabled |\n\n") 4730 4731 info.extend([ 4732 "\n## Current user tariff limits\n", 4733 "\n### See also\n", 4734 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4735 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4736 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4737 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4738 "\n### Unary limits\n", 4739 ]) 4740 4741 if unary: 4742 for key, values in sorted(unary.items()): 4743 info.append("\n* Max requests per minute: {}\n".format(key)) 4744 4745 for value in values: 4746 info.append(" - {}\n".format(value)) 4747 4748 else: 4749 info.append("\nNot available\n") 4750 4751 info.append("\n### Stream limits\n") 4752 4753 if stream: 4754 for key, values in sorted(stream.items()): 4755 info.append("\n* Max stream connections: {}\n".format(key)) 4756 4757 for value in values: 4758 info.append(" - {}\n".format(value)) 4759 4760 else: 4761 info.append("\nNot available\n") 4762 4763 infoText = "".join(info) 4764 4765 if show and not onlyFiles: 4766 uLogger.info(infoText) 4767 4768 if self.userInfoFile and (show or onlyFiles): 4769 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4770 fH.write(infoText) 4771 4772 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4773 4774 if self.useHTMLReports: 4775 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4776 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4777 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4778 4779 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4780 4781 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4784class Args: 4785 """ 4786 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4787 """ 4788 def __init__(self, **kwargs): 4789 self.__dict__.update(kwargs) 4790 4791 def __getattr__(self, item): 4792 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4795def ParseArgs(): 4796 """This function get and parse command line keys.""" 4797 parser = ArgumentParser() # command-line string parser 4798 4799 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4800 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4801 4802 # --- options: 4803 4804 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4805 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4806 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4807 4808 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4809 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4810 4811 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4812 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4813 4814 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4815 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4816 4817 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4818 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4819 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4820 4821 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4822 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4823 parser.add_argument("--tag", type=str, default="", help="Option: identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).") 4824 4825 # --- commands: 4826 4827 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4828 4829 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4830 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4831 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4832 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4833 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4834 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4835 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4836 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4837 4838 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4839 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4840 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4841 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4842 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4843 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4844 4845 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4846 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4847 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4848 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4849 4850 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4851 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4852 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4853 4854 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4855 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4856 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4857 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4858 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4859 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4860 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4861 4862 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4863 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4864 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4865 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4866 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4867 4868 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4869 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4870 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4871 4872 cmdArgs = parser.parse_args() 4873 return cmdArgs
This function get and parse command line keys.
4876def Main(**kwargs): 4877 """ 4878 Main function for work with TKSBrokerAPI in the console. 4879 4880 See examples: 4881 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4882 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4883 """ 4884 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4885 4886 if args.debug_level: 4887 uLogger.level = 10 # always debug level by default 4888 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4889 4890 exitCode = 0 4891 start = datetime.now(tzutc()) 4892 uLogger.debug("=-" * 50) 4893 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4894 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4895 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4896 )) 4897 4898 # trying to calculate full current version: 4899 buildVersion = __version__ 4900 try: 4901 v = version("tksbrokerapi") 4902 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4903 4904 except Exception: 4905 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4906 4907 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4908 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4909 4910 try: 4911 if args.version: 4912 print("TKSBrokerAPI {}".format(buildVersion)) 4913 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4914 4915 else: 4916 # Init class for trading with Tinkoff Broker: 4917 trader = TinkoffBrokerServer( 4918 token=args.token, 4919 accountId=args.account_id, 4920 useCache=not args.no_cache, 4921 ) 4922 4923 if args.tag is not None: 4924 trader.tag = args.tag # Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode 4925 4926 # --- set some options: 4927 4928 if args.more: 4929 trader.moreDebug = True 4930 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4931 4932 if args.html: 4933 trader.useHTMLReports = True 4934 4935 if args.ticker: 4936 ticker = str(args.ticker).upper() # Tickers may be upper case only 4937 4938 if ticker in trader.aliasesKeys: 4939 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4940 4941 else: 4942 trader.ticker = ticker 4943 4944 if args.figi: 4945 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4946 4947 if args.depth is not None: 4948 trader.depth = args.depth 4949 4950 # --- do one command: 4951 4952 if args.list: 4953 if args.output is not None: 4954 trader.instrumentsFile = args.output 4955 4956 trader.ShowInstrumentsInfo(show=True) 4957 4958 elif args.list_xlsx: 4959 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4960 4961 elif args.bonds_xlsx is not None: 4962 if args.output is not None: 4963 trader.bondsXLSXFile = args.output 4964 4965 if len(args.bonds_xlsx) == 0: 4966 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4967 4968 else: 4969 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4970 4971 elif args.search: 4972 if args.output is not None: 4973 trader.searchResultsFile = args.output 4974 4975 trader.SearchInstruments(pattern=args.search[0], show=True) 4976 4977 elif args.info: 4978 if not (args.ticker or args.figi): 4979 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4980 raise Exception("Ticker or FIGI required") 4981 4982 if args.output is not None: 4983 trader.infoFile = args.output 4984 4985 if args.ticker: 4986 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4987 4988 else: 4989 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4990 4991 elif args.calendar is not None: 4992 if args.output is not None: 4993 trader.calendarFile = args.output 4994 4995 if len(args.calendar) == 0: 4996 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4997 4998 else: 4999 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 5000 5001 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 5002 5003 elif args.price: 5004 if not (args.ticker or args.figi): 5005 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5006 raise Exception("Ticker or FIGI required") 5007 5008 trader.GetCurrentPrices(show=True) 5009 5010 elif args.prices is not None: 5011 if args.output is not None: 5012 trader.pricesFile = args.output 5013 5014 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 5015 5016 elif args.overview: 5017 if args.output is not None: 5018 trader.overviewFile = args.output 5019 5020 trader.Overview(show=True, details="full") 5021 5022 elif args.overview_digest: 5023 if args.output is not None: 5024 trader.overviewDigestFile = args.output 5025 5026 trader.Overview(show=True, details="digest") 5027 5028 elif args.overview_positions: 5029 if args.output is not None: 5030 trader.overviewPositionsFile = args.output 5031 5032 trader.Overview(show=True, details="positions") 5033 5034 elif args.overview_orders: 5035 if args.output is not None: 5036 trader.overviewOrdersFile = args.output 5037 5038 trader.Overview(show=True, details="orders") 5039 5040 elif args.overview_analytics: 5041 if args.output is not None: 5042 trader.overviewAnalyticsFile = args.output 5043 5044 trader.Overview(show=True, details="analytics") 5045 5046 elif args.overview_calendar: 5047 if args.output is not None: 5048 trader.overviewAnalyticsFile = args.output 5049 5050 trader.Overview(show=True, details="calendar") 5051 5052 elif args.deals is not None: 5053 if args.output is not None: 5054 trader.reportFile = args.output 5055 5056 if 0 <= len(args.deals) < 3: 5057 trader.Deals( 5058 start=args.deals[0] if len(args.deals) >= 1 else None, 5059 end=args.deals[1] if len(args.deals) == 2 else None, 5060 show=True, # Always show deals report in console 5061 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 5062 ) 5063 5064 else: 5065 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5066 raise Exception("Incorrect value") 5067 5068 elif args.history is not None: 5069 if args.output is not None: 5070 trader.historyFile = args.output 5071 5072 if 0 <= len(args.history) < 3: 5073 dataReceived = trader.History( 5074 start=args.history[0] if len(args.history) >= 1 else None, 5075 end=args.history[1] if len(args.history) == 2 else None, 5076 interval="hour" if args.interval is None or not args.interval else args.interval, 5077 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5078 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5079 show=True, # shows all downloaded candles in console 5080 ) 5081 5082 if args.render_chart is not None and dataReceived is not None: 5083 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5084 5085 trader.ShowHistoryChart( 5086 candles=dataReceived, 5087 interact=iChart, 5088 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5089 ) 5090 5091 else: 5092 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5093 raise Exception("Incorrect value") 5094 5095 elif args.load_history is not None: 5096 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5097 5098 if args.render_chart is not None and histData is not None: 5099 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5100 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5101 5102 trader.ShowHistoryChart( 5103 candles=histData, 5104 interact=iChart, 5105 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5106 ) 5107 5108 elif args.trade is not None: 5109 if 1 <= len(args.trade) <= 5: 5110 trader.Trade( 5111 operation=args.trade[0], 5112 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5113 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5114 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5115 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5116 ) 5117 5118 else: 5119 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5120 5121 elif args.buy is not None: 5122 if 0 <= len(args.buy) <= 4: 5123 trader.Buy( 5124 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5125 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5126 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5127 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5128 ) 5129 5130 else: 5131 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5132 5133 elif args.sell is not None: 5134 if 0 <= len(args.sell) <= 4: 5135 trader.Sell( 5136 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5137 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5138 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5139 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5140 ) 5141 5142 else: 5143 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5144 5145 elif args.order: 5146 if 4 <= len(args.order) <= 7: 5147 trader.Order( 5148 operation=args.order[0], 5149 orderType=args.order[1], 5150 lots=int(args.order[2]), 5151 targetPrice=float(args.order[3]), 5152 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5153 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5154 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5155 ) 5156 5157 else: 5158 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5159 5160 elif args.buy_limit: 5161 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5162 5163 elif args.sell_limit: 5164 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5165 5166 elif args.buy_stop: 5167 if 2 <= len(args.buy_stop) <= 7: 5168 trader.BuyStop( 5169 lots=int(args.buy_stop[0]), 5170 targetPrice=float(args.buy_stop[1]), 5171 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5172 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5173 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5174 ) 5175 5176 else: 5177 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5178 5179 elif args.sell_stop: 5180 if 2 <= len(args.sell_stop) <= 7: 5181 trader.SellStop( 5182 lots=int(args.sell_stop[0]), 5183 targetPrice=float(args.sell_stop[1]), 5184 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5185 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5186 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5187 ) 5188 5189 else: 5190 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5191 5192 # elif args.buy_order_grid is not None: 5193 # # update order grid work with api v2 5194 # if len(args.buy_order_grid) == 2: 5195 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5196 # 5197 # for order in orderParams: 5198 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5199 # 5200 # else: 5201 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5202 # 5203 # elif args.sell_order_grid is not None: 5204 # # update order grid work with api v2 5205 # if len(args.sell_order_grid) >= 2: 5206 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5207 # 5208 # for order in orderParams: 5209 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5210 # 5211 # else: 5212 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5213 5214 elif args.close_order is not None: 5215 trader.CloseOrders(args.close_order) # close only one order 5216 5217 elif args.close_orders is not None: 5218 trader.CloseOrders(args.close_orders) # close list of orders 5219 5220 elif args.close_trade: 5221 if not (args.ticker or args.figi): 5222 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5223 raise Exception("Ticker or FIGI required") 5224 5225 if args.ticker: 5226 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5227 5228 else: 5229 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5230 5231 elif args.close_trades is not None: 5232 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5233 5234 elif args.close_all is not None: 5235 if args.ticker: 5236 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5237 5238 elif args.figi: 5239 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5240 5241 else: 5242 trader.CloseAll(*args.close_all) 5243 5244 elif args.limits: 5245 if args.output is not None: 5246 trader.withdrawalLimitsFile = args.output 5247 5248 trader.OverviewLimits(show=True) 5249 5250 elif args.user_info: 5251 if args.output is not None: 5252 trader.userInfoFile = args.output 5253 5254 trader.OverviewUserInfo(show=True) 5255 5256 elif args.account: 5257 if args.output is not None: 5258 trader.userAccountsFile = args.output 5259 5260 trader.OverviewAccounts(show=True) 5261 5262 else: 5263 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5264 raise Exception("There is no command to execute") 5265 5266 except Exception: 5267 trace = tb.format_exc() 5268 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5269 if e in trace: 5270 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5271 break 5272 5273 uLogger.debug(trace) 5274 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5275 exitCode = 255 # an error occurred, must be open a ticket for this issue 5276 5277 finally: 5278 finish = datetime.now(tzutc()) 5279 5280 if exitCode == 0: 5281 if args.more: 5282 uLogger.debug("All operations were finished success (summary code is 0).") 5283 5284 else: 5285 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5286 os.path.abspath(uLog.defaultLogFile), exitCode, 5287 )) 5288 5289 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5290 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5291 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5292 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5293 )) 5294 uLogger.debug("=-" * 50) 5295 5296 if not kwargs: 5297 sys.exit(exitCode) 5298 5299 else: 5300 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples:
